Overview
<Slider /> is the canonical numeric range input. One component, two modes:
- Single — one thumb, returns
number. - Range — N≥2 thumbs, returns
number[](sorted, constrained byminStepsBetweenThumbs).
It implements the W3C Slider authoring pattern in full: each thumb is its own role="slider",
focusable, with arrow / page / home / end keyboard support. Pointer-down on the track jumps the
nearest thumb. Vertical orientation reads top-as-max in both LTR and RTL.
The thumb scale-up on drag and the fill width transitions are pure CSS — no Motion-library
import. prefers-reduced-motion collapses them via Tailwind's motion-reduce: variant.
Overview — range with marks and a continuous slider
Anatomy
[ track [ fill ] [ mark · mark · mark ] [ thumb ] [ value label ] ]
[ thumb ] [ value label ] (range mode)[ track [ fill ] [ mark · mark · mark ] [ thumb ] [ value label ] ]
[ thumb ] [ value label ] (range mode)- track — the off-state rail. Variant changes its tone but not its length.
- fill — the on-state colored portion. Single mode:
0% → thumb. Range mode:lo → hi. - thumb — each
role="slider".data-draggingscales it up,data-focus-visiblepaints the focus ring colored to match the active palette. - mark — small tick dot at a given mark value (optionally with a label).
- valueLabel — floating value bubble above (horizontal) / beside (vertical) each thumb.
The thumbs are the only interactive elements in the a11y tree. The track + fill + marks are
aria-hidden="true". The hidden <input type="range"> siblings exist purely for form
submission.
Examples
Basic
Range
Marks
Vertical
ValueLabel
FormattedValue
Variants
Sizes
Colors
Disabled
Invalid
Continuous
ManyThumbs
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| aria-label | string | — | Accessible label for the slider as a whole. In range mode, applied to the root `role=group`. |
| aria-labelledby | string | — | ID of an external label element; applied to root in range mode, to thumb in single mode. |
| className | string | — | Override className on the root. Wins over recipe + theme via tailwind-merge. |
| color | ResponsiveValue<SliderColor> | 'primary' | Palette role for the filled track + thumb accent. |
| defaultValue | SliderValue | — | Uncontrolled initial value. |
| disabled | boolean | — | Block all interaction; visual + a11y disabled. |
| formatValue | ((value: number) => ReactNode) | `(v) => String(v)` | Custom formatter for the value bubble content. |
| getAriaValueText | ((value: number, index: number) => string) | value-based ("Minimum value", "Maximum value", or "Value") | Per-thumb a11y label text. Receives the thumb's value and (in range mode) the thumb's index. |
| getThumbAriaLabel | ((index: number) => string) | — | Per-thumb `aria-label`. Range default: `['Minimum value', 'Maximum value', …]`. Single default: inherits the root `aria-label` if set, else "Value". |
| invalid | boolean | — | Visual + a11y invalid state (red ring on thumbs). |
| marks | SliderMark[] | — | Marks rendered as tick dots on the track; `label` adds a caption below (or beside). |
| max | number | 100 | Upper bound. |
| min | number | 0 | Lower bound. |
| minStepsBetweenThumbs | number | 1 | Range mode only — minimum number of `step`s between adjacent thumbs. Defaults to `1` so thumbs can touch but not cross. Set to `0` to allow overlap. Ignored when `step` is `null`. |
| mode | enum | 'single' | Single thumb vs N-thumb range. |
| name | string | — | Hidden `<input type="range">` name for form submission. In range mode, suffix `-min` / `-max` are appended (or `-0` / `-1` / … for N-thumb sliders). |
| onChange | ((value: SliderValue) => void) | — | Fires on every value commit during drag and every keyboard step. |
| onChangeEnd | ((value: SliderValue) => void) | — | Fires once on pointerup / keyup — useful for splitting "preview" from "expensive write". |
| orientation | enum | 'horizontal' | Layout direction. |
| renderValueLabel | ((formatted: ReactNode, value: number) => ReactNode) | — | Slot for a custom value-label renderer (e.g. wrapping a Tooltip). Receives the value already formatted by `formatValue`. The default returns a small floating tag in the DS's emphasis tone. |
| showTicks | boolean | — | Show a small tick at every `step` value (off by default — only on for sparse step counts). |
| showValueLabel | enum | 'never' | Behavior of the floating value bubble above each thumb. |
| size | ResponsiveValue<SliderSize> | 'md' | Track thickness + thumb diameter scale. |
| step | number | null | 1 | Discrete step. Pass `null` for a continuous slider (no snapping; smooth pointer drag). |
| style | CSSProperties | — | Standard inline style. Wins over `sx` for shared keys. |
| sx | Sx | — | Theme-aware inline style object. |
| value | SliderValue | — | Controlled value. `number` for single, `number[]` for range. |
| variant | ResponsiveValue<SliderVariant> | 'solid' | Stylistic family. |
Accessibility
- Each thumb is
role="slider"witharia-valuemin/aria-valuemax/aria-valuenow/aria-valuetext/aria-orientation. Each thumb is independently focusable via Tab. - Range mode wraps thumbs in
role="group"witharia-label/aria-labelledbyfrom the root props, so screen readers announce the group name before each thumb's value. - Per-thumb labels default to
"Minimum value"/"Maximum value"for two-thumb ranges and"Value N"for N-thumb ranges. Override withgetThumbAriaLabel(index). - Value text is read from
aria-valuetext. UsegetAriaValueText(value, index)for domain-specific strings ("$12.99", "1:42 remaining"). - W3C Slider keyboard pattern:
←/→→ ±step (horizontal RTL mirrored automatically)↑/↓→ ±step (always direction-agnostic)Shift + arrow→ ±10 stepsPageUp/PageDown→ ±10 stepsHome/End→ min / max (clamped by neighbor thumbs in range mode)
invalidsetsaria-invalidon every thumb and adds a danger ring.disabledsetsaria-disabledon every thumb, removes them from the tab order, and blocks pointer + keyboard interaction.- axe-core passes across the 4 variants × 7 colors × 3 sizes matrix, plus disabled / invalid / range / vertical / continuous states.
- Touch targets meet WCAG 2.5.5 — the visible thumb is 14 / 18 / 22 px but the recipe pads
the hit area via
paddingon focus / hover so finger taps always have ≥ 24-px reach.
Theming
<ThemeProvider
theme={defineTheme({
components: {
Slider: {
defaultProps: { variant: 'soft', size: 'lg', color: 'success' },
styleOverrides: {
track: 'shadow-inner',
thumb: 'shadow-md ring-offset-2',
fill: 'shadow-sm',
valueLabel: 'rounded-full bg-success text-success-contrast',
},
},
},
})}
>
{children}
</ThemeProvider><ThemeProvider
theme={defineTheme({
components: {
Slider: {
defaultProps: { variant: 'soft', size: 'lg', color: 'success' },
styleOverrides: {
track: 'shadow-inner',
thumb: 'shadow-md ring-offset-2',
fill: 'shadow-sm',
valueLabel: 'rounded-full bg-success text-success-contrast',
},
},
},
})}
>
{children}
</ThemeProvider>Instance-level overrides win via tailwind-merge:
<Slider className="opacity-90" sx={{ radius: 'lg' }}>
…
</Slider><Slider className="opacity-90" sx={{ radius: 'lg' }}>
…
</Slider>Pointer interaction
- Track click — picks the nearest thumb (lowest-index tie-break), focuses it, jumps it to the pointer position, and starts a drag.
- Thumb drag —
pointerdownon a thumb starts a drag without jumping the position; the thumb tracks the pointer until release. Listeners are attached towindow, so the drag survives the pointer leaving the track (matches native<input type="range">). - Continuous mode (
step={null}) — no snapping. The thumb follows the pointer position to the fractional value. Useful for analog scrub bars; pair withformatValueto round just the displayed text.
RTL
- Horizontal: pointer X is mirrored automatically (rightward gesture still means "toward
max"). Arrow keys are mirrored to match. Use
<DirectionProvider dir="rtl">or set<html dir="rtl">. - Vertical: direction-agnostic. Top is always max in both LTR and RTL.
Form participation
Pass name to emit hidden <input type="range"> elements (one per thumb):
<Slider name="volume" defaultValue={70} /> // FormData → volume=70
<Slider name="price" mode="range" defaultValue={[10, 50]} />
// → FormData → price-min=10, price-max=50
<Slider name="band" mode="range" defaultValue={[10, 30, 60]} />
// → FormData → band-0=10, band-1=30, band-2=60<Slider name="volume" defaultValue={70} /> // FormData → volume=70
<Slider name="price" mode="range" defaultValue={[10, 50]} />
// → FormData → price-min=10, price-max=50
<Slider name="band" mode="range" defaultValue={[10, 30, 60]} />
// → FormData → band-0=10, band-1=30, band-2=60The native <input type="range"> is the right shape for form submission — every backend that
already speaks form-encoded numbers reads these correctly without a schema change.
Do / Don't
- Do use Slider for bounded numeric input where the bounds are meaningful (volume, opacity, price filters, EQ bands).
- Do pair
onChangewithonChangeEndwhen the commit is expensive: render a preview during drag, persist on release. - Do use
step={null}for analog-feel scrubbers (audio playhead, color picker). - Don't use Slider for unbounded numeric input — that's
<NumberInput />. - Don't use Slider for categorical selection — that's
<RadioGroup />or<ToggleGroup />. Slider implies a continuous scale, even at low step counts. - Don't rely on the visible value label as the only way to read the value — assistive tech
reads
aria-valuetext. If your formatted text matters, setgetAriaValueText.