A slider allows users to make selections from a range of values. It is a custom <input type="range"> with support for custom styling, accessibility, multi-thumb, vertical orientation, and RTL.
Features
- Supports centered origin (track fills from center to thumb)
- Full keyboard navigation with proper ARIA
- Click or touch on track to update value
- Right-to-left (RTL) support
- Horizontal and vertical orientations
- Multi-thumb range selection
- Prevents text selection while dragging
- Tick mark rendering
Installation
npm install @zag-js/slider @zag-js/react
import { useMachine, normalizeProps } from "@zag-js/react"
import * as slider from "@zag-js/slider"
import { useId } from "react"
export function Slider() {
const service = useMachine(slider.machine, {
id: useId(),
defaultValue: [40],
min: 0,
max: 100,
})
const api = slider.connect(service, normalizeProps)
return (
<div {...api.getRootProps()}>
<div>
<label {...api.getLabelProps()}>Volume</label>
<output {...api.getValueTextProps()}>{api.value[0]}</output>
</div>
<div {...api.getControlProps()}>
<div {...api.getTrackProps()}>
<div {...api.getRangeProps()} />
</div>
{api.value.map((_, i) => (
<div key={i} {...api.getThumbProps({ index: i })}>
<input {...api.getHiddenInputProps({ index: i })} />
</div>
))}
</div>
</div>
)
}
<script setup>
import { useMachine, normalizeProps } from "@zag-js/vue"
import * as slider from "@zag-js/slider"
import { useId, computed } from "vue"
const service = useMachine(slider.machine, {
id: useId(),
defaultValue: [40],
})
const api = computed(() => slider.connect(service, normalizeProps))
</script>
<template>
<div v-bind="api.getRootProps()">
<label v-bind="api.getLabelProps()">Volume</label>
<div v-bind="api.getControlProps()">
<div v-bind="api.getTrackProps()">
<div v-bind="api.getRangeProps()" />
</div>
<div
v-for="(_, i) in api.value"
:key="i"
v-bind="api.getThumbProps({ index: i })"
>
<input v-bind="api.getHiddenInputProps({ index: i })" />
</div>
</div>
</div>
</template>
Setting the initial value
Pass defaultValue as an array. Use a single-element array for a single thumb.
const service = useMachine(slider.machine, {
id: useId(),
defaultValue: [30],
})
Controlled slider
Use value and onValueChange to control the value externally.
const [value, setValue] = useState([40])
const service = useMachine(slider.machine, {
id: useId(),
value,
onValueChange(details) {
// details => { value: number[] }
setValue(details.value)
},
})
Min, max, and step
const service = useMachine(slider.machine, {
id: useId(),
min: -10,
max: 10,
step: 0.5,
})
Vertical orientation
const service = useMachine(slider.machine, {
id: useId(),
orientation: "vertical",
})
Apply a height style to the root element when using vertical orientation.
Track origin
Control where the filled track starts with origin:
"start" — track fills from start to thumb (default)
"center" — track fills from the midpoint (50%) to the thumb; useful for offset/diverging scales
"end" — track fills from thumb to the end; useful for threshold sliders
const service = useMachine(slider.machine, {
id: useId(),
origin: "center",
})
Multi-thumb range slider
Pass an array with two values for a range slider.
const service = useMachine(slider.machine, {
id: useId(),
defaultValue: [20, 80],
})
Thumb collision behavior
For multi-thumb sliders, control what happens when thumbs overlap.
const service = useMachine(slider.machine, {
id: useId(),
defaultValue: [20, 80],
thumbCollisionBehavior: "swap", // "none" | "push" | "swap"
})
Thumb alignment
Control whether the thumb center aligns to the track or stays contained within it.
const service = useMachine(slider.machine, {
id: useId(),
thumbAlignment: "contain",
thumbSize: { width: 20, height: 20 },
})
RTL support
const service = useMachine(slider.machine, {
id: useId(),
dir: "rtl",
})
Tick marks
Use api.getMarkerProps() to position markers along the track.
<div {...api.getMarkerGroupProps()}>
{[0, 25, 50, 75, 100].map((value) => (
<span key={value} {...api.getMarkerProps({ value })}>
{value}
</span>
))}
</div>
Usage within forms
Set name and render the hidden input via api.getHiddenInputProps().
const service = useMachine(slider.machine, {
id: useId(),
name: "volume",
})
API reference
The initial value(s) of the slider (uncontrolled).
The controlled value(s). Use with onValueChange.
The minimum allowed value. Defaults to 0.
The maximum allowed value. Defaults to 100.
The step increment. Defaults to 1.
orientation
"horizontal" | "vertical"
The orientation of the slider. Defaults to "horizontal".
origin
"start" | "center" | "end"
Where the track fill originates. Defaults to "start".
How the thumb aligns relative to the track bounds. Defaults to "contain".
thumbSize
{ width: number, height: number }
The explicit size of the thumb. Used when thumbAlignment is "contain".
What happens when two thumbs collide in a multi-thumb slider.
Whether the slider is disabled.
Whether the slider is in an invalid state.
The name used for the hidden input in form submissions.
The text direction. Defaults to "ltr".
getAriaValueText
(details: { value: number, index: number }) => string
Returns the aria-valuetext for a given thumb. Used for screen reader announcements.
onValueChange
(details: { value: number[] }) => void
Callback fired while dragging or on keyboard input.
onValueChangeEnd
(details: { value: number[] }) => void
Callback fired when dragging ends or on keyboard commit.
onFocusChange
(details: { focusedIndex: number, value: number[] }) => void
Callback fired when focus moves between thumbs.
Styling
Each part has a data-part attribute. State attributes are added to the relevant parts.
Focused state
[data-part="root"][data-focus] { /* root when any thumb is focused */ }
[data-part="control"][data-focus] { /* control when focused */ }
[data-part="track"][data-focus] { /* track when focused */ }
[data-part="range"][data-focus] { /* range when focused */ }
Disabled state
[data-part="root"][data-disabled] { /* disabled root */ }
[data-part="label"][data-disabled] { /* disabled label */ }
[data-part="thumb"][data-disabled] { /* disabled thumb */ }
[data-part="range"][data-disabled] { /* disabled range */ }
Invalid state
[data-part="root"][data-invalid] { /* invalid root */ }
[data-part="label"][data-invalid] { /* invalid label */ }
[data-part="thumb"][data-invalid] { /* invalid thumb */ }
[data-part="range"][data-invalid] { /* invalid range */ }
Orientation
[data-part="root"][data-orientation="horizontal"] { /* horizontal root */ }
[data-part="root"][data-orientation="vertical"] { /* vertical root */ }
[data-part="track"][data-orientation="horizontal"] { /* horizontal track */ }
[data-part="track"][data-orientation="vertical"] { /* vertical track */ }
Tick marks
[data-part="marker"][data-state="at-value"] { /* marker at current value */ }
[data-part="marker"][data-state="under-value"] { /* marker below current value */ }
[data-part="marker"][data-state="over-value"] { /* marker above current value */ }
Accessibility
Adheres to the Slider WAI-ARIA design pattern.
Keyboard interactions
| Key | Description |
|---|
ArrowRight / ArrowUp | Increments the focused thumb by the step |
ArrowLeft / ArrowDown | Decrements the focused thumb by the step |
PageUp | Increments by a larger step |
PageDown | Decrements by a larger step |
Home | Sets the focused thumb to the minimum value |
End | Sets the focused thumb to the maximum value |