Slider
A slider allows users to make selections from a range of values. Think of it as
a custom <input type='range'/>
with the ability to achieve custom styling and
accessibility.
Features
- Supports centered origin (slider starting at center, instead of start position).
- Fully managed keyboard navigation.
- Supports touch or click on track to update value.
- Supports Right-to-Left directionality.
- Support for horizontal and vertical orientations.
- Prevents text selection while dragging.
Installation
To use the slider machine in your project, run the following command in your command line:
npm install @zag-js/slider @zag-js/react # or yarn add @zag-js/slider @zag-js/react
npm install @zag-js/slider @zag-js/solid # or yarn add @zag-js/slider @zag-js/solid
npm install @zag-js/slider @zag-js/vue # or yarn add @zag-js/slider @zag-js/vue
npm install @zag-js/slider @zag-js/vue # or yarn add @zag-js/slider @zag-js/vue
This command will install the framework agnostic slider logic and the reactive utilities for your framework of choice.
Anatomy
To set up the slider correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the slider package into your project
import * as slider from "@zag-js/slider"
The slider package exports two key functions:
machine
— The state machine logic for the slider widget as described in the WAI-ARIA spec.connect
— The function that translates the machine's state to JSX attributes and event handlers.
You'll need to provide a unique
id
to theuseMachine
hook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the slider machine in your project 🔥
import * as slider from "@zag-js/slider" import { useMachine, normalizeProps } from "@zag-js/react" export function Slider() { const [state, send] = useMachine(slider.machine({ id: "1", value: [0] })) const api = slider.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> <div> <label {...api.labelProps}>Slider Label</label> <output {...api.valueTextProps}>{api.value.at(0)}</output> </div> <div {...api.controlProps}> <div {...api.trackProps}> <div {...api.rangeProps} /> </div> {api.value.map((_, index) => ( <div key={index} {...api.getThumbProps({ index })}> <input {...api.getHiddenInputProps({ index })} /> </div> ))} </div> </div> ) }
import * as slider from "@zag-js/slider" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, For, createUniqueId } from "solid-js" export function Slider() { const [state, send] = useMachine( slider.machine({ id: createUniqueId(), value: [0] }), ) const api = createMemo(() => slider.connect(state, send, normalizeProps)) return ( <div {...api().rootProps}> <div> <label {...api().labelProps}>Slider Label</label> <output {...api().valueTextProps}>{api().value.at(0)}</output> </div> <div {...api().controlProps}> <div {...api().trackProps}> <div {...api().rangeProps} /> </div> <For each={api().value}> {(_, index) => ( <div {...api().getThumbProps({ index: index() })}> <input {...api().getHiddenInputProps({ index: index() })} /> </div> )} </For> </div> </div> ) }
import * as slider from "@zag-js/slider" import { normalizeProps, useMachine } from "@zag-js/vue" import { defineComponent, computed, h, Fragment } from "vue" export default defineComponent({ name: "Slider", setup() { const [state, send] = useMachine(slider.machine({ id: "1", value: [0] })) const apiRef = computed(() => slider.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <div {...api.rootProps}> <div> <label {...api.labelProps}>Slider Label</label> <output {...api.valueTextProps}>{api.value.at(0)}</output> </div> <div {...api.controlProps}> <div {...api.trackProps}> <div {...api.rangeProps} /> </div> {api.value.map((_, index) => ( <div key={index} {...api.getThumbProps({ index })}> <input {...api.getHiddenInputProps({ index })} /> </div> ))} </div> </div> ) } }, })
<script setup> import * as slider from "@zag-js/slider"; import { normalizeProps, useMachine } from "@zag-js/vue"; import { computed } from "vue"; const [state, send] = useMachine(slider.machine({ id: "1", value: [0] })); const api = computed(() => slider.connect(state.value, send, normalizeProps)); </script> <template> <div ref="ref" v-bind="api.rootProps"> <div> <label v-bind="api.labelProps">Slider Label</label> <output v-bind="api.valueTextProps">{{ api.value.at(0) }}</output> </div> <div v-bind="api.controlProps"> <div v-bind="api.trackProps"> <div v-bind="api.rangeProps" /> </div> <div v-for="(_, index) in api.value" :key="index" v-bind="api.getThumbProps({ index })" > <input v-bind="api.getHiddenInputProps({ index })" /> </div> </div> </div> </template>
Changing the orientation
By default, the slider is assumed to be horizontal. To change the orientation to
vertical, set the orientation
property in the machine's context to vertical
.
In this mode, the slider will use the arrow up and down keys to increment/decrement its value.
Don't forget to change the styles of the vertical slider by specifying its height
const [state, send] = useMachine( slider.machine({ orientation: "vertical", }), )
Setting the initial value
const [state, send] = useMachine( slider.machine({ value: [30], }), )
Specifying the minimum and maximum
By default, the minimum is 0
and the maximum is 100
. If that's not what you
want, you can easily specify different bounds by changing the values of the min
and/or max attributes.
For example, to ask the user for a value between -10
and 10
, you can use:
const [state, send] = useMachine( slider.machine({ min: -10, max: 10, }), )
Setting the value's granularity
By default, the granularity, is 1
, meaning that the value is always an
integer. You can change the step attribute to control the granularity.
For example, If you need a value between 5
and 10
, accurate to two decimal
places, you should set the value of step to 0.01
:
const [state, send] = useMachine( slider.machine({ min: 5, max: 10, step: 0.01, }), )
Listening for changes
When the slider value changes, the onValueChange
and onValueChangeEnd
callbacks are invoked. You can use this to setup custom behaviors in your app.
const [state, send] = useMachine( slider.machine({ onValueChange(details) { console.log("value is changing to:", details) }, onValueChangeEnd(details) { console.log("value has changed to:", details) }, }), )
Changing the start position
By default, the slider's "zero position" is usually at the start position (left in LTR and right in RTL).
In scenarios where the value represents an offset (or relative value), it might
be useful to change the "zero position" to center. To do this, pass the origin
context property to center
.
const [state, send] = useMachine( slider.machine({ origin: "center", }), )
Usage within forms
To use slider within forms, use the exposed inputProps
from the connect
function and ensure you pass name
value to the machine's context. It will
render a hidden input and ensure the value changes get propagated to the form
correctly.
const [state, send] = useMachine( slider.machine({ name: "quantity", }), )
RTL Support
The slider has built-in support for RTL alignment and interaction. In the RTL mode, operations are performed from right to left, meaning, the left arrow key will increment and the right arrow key will decrement.
To enable RTL support, pass the dir: rtl
context property
const [state, send] = useMachine( slider.machine({ dir: "rtl", }), )
While we take care of the interactions in RTL mode, you'll have to ensure you apply the correct CSS styles to flip the layout.
Using slider marks
To show marks or ticks along the slider track, use the exposed
api.getMarkerProps()
method to position the slider marks relative to the
track.
//... <div> <div {...api.controlProps}> <div {...api.trackProps}> <div {...api.rangeProps} /> </div> {api.value.map((_, index) => ( <div key={index} {...api.getThumbProps({ index })}> <input {...api.getHiddenInputProps({ index })} /> </div> ))} </div> <div {...api.markerGroupProps}> <span {...api.getMarkerProps({ value: 10 })}>|</span> <span {...api.getMarkerProps({ value: 30 })}>|</span> <span {...api.getMarkerProps({ value: 90 })}>|</span> </div> </div> //...
//... <div> <div {...api().controlProps}> <div {...api().trackProps}> <div {...api().rangeProps} /> </div> <div {...api().getThumbProps({ index: 0 })}> <input {...api().getHiddenInputProps({ index: 0 })} /> </div> </div> <div {...api().markerGroupProps}> <span {...api().getMarkerProps({ value: 10 })}>|</span> <span {...api().getMarkerProps({ value: 30 })}>|</span> <span {...api().getMarkerProps({ value: 90 })}>|</span> </div> </div> //...
//... <div> <div {...api.controlProps}> <div {...api.trackProps}> <div {...api.rangeProps} /> </div> <div {...api.getThumbProps({ index: 0 })}> <input {...api.getHiddenInputProps({ index: 0 })} /> </div> </div> <div {...api.markerGroupProps}> <span {...api.getMarkerProps({ value: 10 })}>|</span> <span {...api.getMarkerProps({ value: 30 })}>|</span> <span {...api.getMarkerProps({ value: 90 })}>|</span> </div> </div> //...
//... <div> <div v-bind="api.controlProps"> <div v-bind="api.trackProps"> <div v-bind="api.rangeProps" /> </div> <div v-bind="api.getThumbProps({ index: 0 })"> <input v-bind="api.getHiddenInputProps({ index: 0 })" /> </div> </div> <div v-bind="api.markerGroupProps"> <span v-bind="api.getMarkerProps({ value: 10 })">|</span> <span v-bind="api.getMarkerProps({ value: 30 })">|</span> <span v-bind="api.getMarkerProps({ value: 90 })">|</span> </div> </div> //...
Styling guide
Earlier, we mentioned that each slider part has a data-part
attribute added to
them to select and style them in the DOM.
Focused State
When the slider thumb is focused, the data-focus
attribute is added to the
root, control, thumb and label parts.
[data-part="root"][data-focus] { /* styles for root focus state */ } [data-part="thumb"]:focus { /* styles for thumb focus state */ } [data-part="control"][data-focus] { /* styles for control focus state */ } [data-part="track"][data-focus] { /* styles for track focus state */ } [data-part="range"][data-focus] { /* styles for range focus state */ }
Disabled State
When the slider is disabled, the data-disabled
attribute is added to the root,
label, control and thumb.
[data-part="root"][data-disabled] { /* styles for root disabled state */ } [data-part="label"][data-disabled] { /* styles for label disabled state */ } [data-part="control"][data-disabled] { /* styles for control disabled state */ } [data-part="value-text"][data-disabled] { /* styles for output disabled state */ } [data-part="thumb"][data-disabled] { /* styles for thumb disabled state */ } [data-part="range"][data-disabled] { /* styles for thumb disabled state */ }
Invalid State
When the slider is invalid, the data-invalid
attribute is added to the root,
track, range, label, and thumb parts.
[data-part="root"][data-invalid] { /* styles for root invalid state */ } [data-part="label"][data-invalid] { /* styles for label invalid state */ } [data-part="control"][data-invalid] { /* styles for control invalid state */ } [data-part="valueText"][data-invalid] { /* styles for output invalid state */ } [data-part="thumb"][data-invalid] { /* styles for thumb invalid state */ } [data-part="range"][data-invalid] { /* styles for range invalid state */ }
Orientation
[data-part="root"][data-orientation="(horizontal|vertical)"] { /* styles for horizontal or vertical */ } [data-part="thumb"][data-orientation="(horizontal|vertical)"] { /* styles for horizontal or vertical */ } [data-part="track"][data-orientation="(horizontal|vertical)"] { /* styles for horizontal or vertical */ }
Styling the markers
[data-part="marker"][data-state="(at|under|over)-value"] { /* styles for when the value exceeds the marker's value */ }
Methods and Properties
Machine Context
The slider machine exposes the following context properties:
ids
Partial<{ root: string; thumb(index: number): string; control: string; track: string; range: string; label: string; valueText: string; marker(index: number): string; }>
The ids of the elements in the range slider. Useful for composition.aria-label
string[]
The aria-label of each slider thumb. Useful for providing an accessible name to the slideraria-labelledby
string[]
The `id` of the elements that labels each slider thumb. Useful for providing an accessible name to the slidername
string
The name associated with each slider thumb (when used in a form)form
string
The associate form of the underlying input element.value
number[]
The value of the range sliderdisabled
boolean
Whether the slider is disabledreadOnly
boolean
Whether the slider is read-onlyinvalid
boolean
Whether the slider is invalidonValueChange
(details: ValueChangeDetails) => void
Function invoked when the value of the slider changesonValueChangeEnd
(details: ValueChangeDetails) => void
Function invoked when the slider value change is doneonFocusChange
(details: FocusChangeDetails) => void
Function invoked when the slider's focused index changesgetAriaValueText
(value: number, index: number) => string
Function that returns a human readable value for the slider thumbmin
number
The minimum value of the slidermax
number
The maximum value of the sliderstep
number
The step value of the sliderminStepsBetweenThumbs
number
The minimum permitted steps between multiple thumbs.orientation
"vertical" | "horizontal"
The orientation of the sliderorigin
"start" | "center"
- "start": Useful when the value represents an absolute value - "center": Useful when the value represents an offset (relative)thumbAlignment
"center" | "contain"
The alignment of the slider thumb relative to the track - `center`: the thumb will extend beyond the bounds of the slider track. - `contain`: the thumb will be contained within the bounds of the track.thumbSize
{ width: number; height: number; }
The slider thumbs dimensionsdir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The slider api
exposes the following methods:
value
number[]
The value of the slider.isDragging
boolean
Whether the slider is being dragged.isFocused
boolean
Whether the slider is focused.setValue
(value: number[]) => void
Function to set the value of the slider.getThumbValue
(index: number) => number
Returns the value of the thumb at the given index.setThumbValue
(index: number, value: number) => void
Sets the value of the thumb at the given index.getValuePercent
(value: number) => number
Returns the percent of the thumb at the given index.getPercentValue
(percent: number) => number
Returns the value of the thumb at the given percent.getThumbPercent
(index: number) => number
Returns the percent of the thumb at the given index.setThumbPercent
(index: number, percent: number) => void
Sets the percent of the thumb at the given index.getThumbMin
(index: number) => number
Returns the min value of the thumb at the given index.getThumbMax
(index: number) => number
Returns the max value of the thumb at the given index.increment
(index: number) => void
Function to increment the value of the slider at the given index.decrement
(index: number) => void
Function to decrement the value of the slider at the given index.focus
() => void
Function to focus the slider. This focuses the first thumb.
Accessibility
Adheres to the Slider WAI-ARIA design pattern.
Keyboard Interactions
- ArrowRightIncrements the slider based on defined step
- ArrowLeftDecrements the slider based on defined step
- ArrowUpIncreases the value by the step amount.
- ArrowDownDecreases the value by the step amount.
- PageUpIncreases the value by a larger step
- PageDownDecreases the value by a larger step
- Shift + ArrowUpIncreases the value by a larger step
- Shift + ArrowDownDecreases the value by a larger step
- HomeSets the value to its minimum.
- EndSets the value to its maximum.
Edit this page on GitHub