Slider
A range input built on Base UI. Discrete and continuous variants, five intents, five sizes, both tones, horizontal/vertical, N-thumb ranges, center-origin fill, dot/line/ruler marks, five value-display modes, and full RTL.
pnpm add @tessinaui/uiWhen to use
- An approximate value on a known range: volume, brightness, opacity. The user cares where the thumb sits, not the exact number.
- A change the user wants to see as they make it. Drag feedback is the point;
pair it with
valueDisplaywhen the number also matters. - A span between two values. Pass an array and each entry gets a thumb, with
minStepsBetweenValueskeeping them apart. - Adjusting around a neutral point.
fillFrom="center"anchors the fill at an origin for bipolar settings. - Named stops instead of numbers. Use
markswithgetValueLabelandgetAriaValueTextso the control speaks the word, not an index.
When not to use
- When the exact number matters. Use NumberField, or add one beside the slider (With number input).
- When the value is not a number. Use Select for a list of choices, or DatePicker for dates.
- For a range that is very wide or very narrow. A wide one makes every position imprecise; a narrow one is better as SegmentedControl or Rating.
- To show a value the user cannot change. Use
Progress or Meter;
readOnlyis for a value that is frozen for now, not a readout. - For a small step up or down. Use Stepper.
Installation
pnpm add @tessinaui/uiUsage
import { Slider } from "@tessinaui/ui";
<Slider label="Volume" defaultValue={40} />Always pass label — it names the control for assistive tech (sr-only by
default, visible with labelPlacement="top").
Examples
Default
Variants
discrete thin-track and continuous capsule fill.
Sizes
Five scales, xs through xl.
Intents
Error message
errorMessage renders under the control, implies intent="error" and joins every thumb's aria-describedby, so each end announces the problem. It is announced politely and suppressed under readOnly.
On-color tone
On a coloured plate the slider inherits tone="on-color" from the enclosing
Surface and paints from the surface ink — legible on any fill by
construction. The capsule also shows iconPlacement="in-track" (the inset
icon: continuous variant only).
Range
Array value → one thumb per entry, any count. minStepsBetweenValues keeps
thumbs from touching.
Center-origin fill
fillFrom="center" (or any value) anchors the fill at an origin instead of the
min end and paints an origin marker — for bipolar ± adjustments.
Value display
valueDisplay: tooltip on interaction, tooltip-always persistent, text
static (pairs with labelPlacement="top"), plus readOnly.
Value in thumb
valueDisplay="thumb" grows the handle into a value pill.
Marks
Dot marks with labels, and the line variant.
Ruler
markVariant="ruler" auto-generates a dense minor/major ruler.
Rating
Discrete stops + a word value via getValueLabel and getAriaValueText.
Vertical dimmer
Segmented
segmented splits the track at each step.
Thumb variants
bar faders, outline rings, and the two handles that read on a capsule
(variant="continuous"): outline for the visible knob, bar for a scrubber
line at the fill's edge.
Scrubber
The control a photo editor sets a number with: the value sits inside the fill
at the leading edge and the line stands where the fill ends, so the bar is the
readout. variant="continuous" + thumbVariant="bar", with the fill and line
stepped down to a settings rail's own chrome.
The bar holds its own value for the length of a drag, so it follows the pointer
even when what it drives updates on a throttle, and the readout is aria-hidden
because the thumb announces the same string through getAriaValueText.
With number input
The default anatomy: slider + synced field, field directly after the slider in the tab order.
With histogram
Presets
External steppers
Non-linear stops
Base UI has no scale transform, so non-linear sliders run in index space over
named stops — the mapping lives in marks, getValueLabel and
getAriaValueText.
With icons
Skeleton
RTL
A horizontal slider mirrors under dir="rtl": the minimum sits at the right edge, the fill grows leftward, and the leading icon and minLabel swap sides. Arrow keys follow the visual direction, and vertical sliders are unaffected. Unset, dir is inherited from the page, arrow keys included.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | readonly number[] | — | Controlled value. Array = one thumb per entry (any count). |
defaultValue | number | readonly number[] | 40 | Initial uncontrolled value. |
min / max / step | number | 0 / 100 / 1 | Range and increment. |
largeStep | number | 10 | Page Up/Down and Shift+Arrow step. |
minStepsBetweenValues | number | 0 | Minimum steps between range thumbs. |
thumbCollisionBehavior | "push" | "swap" | "none" | "push" | Colliding-thumb behaviour. |
onValueChange | (value, eventDetails) => void | — | Every change; eventDetails.reason is 'drag' | 'keyboard' | 'track-press' | …, plus activeThumbIndex. |
onValueCommitted | (value, eventDetails) => void | — | Pointer up / key up. |
variant | "discrete" | "continuous" | "discrete" | Thin track vs capsule. |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Scale. |
intent | "none" | "error" | "warning" | "success" | "info" | "none" | Semantic colour. |
tone | "default" | "on-color" | inherited | Explicit wins; otherwise inherited from Surface. |
orientation | "horizontal" | "vertical" | "horizontal" | |
disabled | boolean | false | Not focusable, not announced. |
readOnly | boolean | false | Focusable and announced, value frozen. |
rounded | "none" | "sm" | "md" | "lg" | "xl" | "full" | "full" | Corner radius of the track, fill and value tooltip, one token step per value. The thumb is round at every value. |
thumbVariant | "default" | "bar" | "outline" | "default" | Handle shape. On continuous, outline renders the visible knob and bar a scrubber line. |
fillFrom | "start" | "center" | number | "start" | Fill anchor (single-thumb); non-start paints an origin marker. |
segmented | boolean | false | Gap at each interior step (discrete, 2–20 segments). |
marks | boolean | SliderMark[] | false | true = every step (thinned >50); array = custom. |
markVariant | "dot" | "line" | "ruler" | "dot" | ruler auto-generates 41 minor/major hashes when marks is unset. |
minLabel / maxLabel | ReactNode | — | Static end labels. |
valueDisplay | "none" | "tooltip" | "tooltip-always" | "text" | "thumb" | "none" | How the live value shows. |
getValueLabel | (value) => ReactNode | — | Custom value rendering ("Okay", "2,500 ml"). |
label | ReactNode | — | Accessible name. Sr-only unless labelPlacement="top". |
labelPlacement | "hidden" | "top" | "hidden" | Visible label row (holds the text value display). |
leadingIcon / trailingIcon | ReactNode | — | End icons. |
iconPlacement | "outside" | "in-track" | "outside" | in-track sits inside the capsule (continuous only). |
getAriaLabel | (index) => string | — | Per-thumb name in range mode. |
getAriaValueText | (formatted, value, index) => string | — | Spoken value. |
name | string | — | Form field name. |
format | Intl.NumberFormatOptions | — | Number formatting (display + aria-valuetext). |
locale | Intl.LocalesArgument | — | Formatting locale. |
errorMessage | ReactNode | — | Error row under the control. Implies intent="error", joins every thumb's aria-describedby, announced politely; suppressed under readOnly. |
dir | "ltr" | "rtl" | — | Wired through Base UI's DirectionProvider — layout AND keyboard flip together. Unset, it inherits the page's direction. |
className | string | — | Root classes. |
Deprecated (kept one minor)
| Old | Use instead |
|---|---|
showValue | valueDisplay="tooltip" |
valueInThumb | valueDisplay="thumb" |
An explicit valueDisplay always wins over the aliases; each alias warns once
in dev.
Migration notes (0.x → this release)
- Radius correction:
rounded="sm"/"md"/"lg"previously rendered one to two steps larger (mdwas 12px). They now sit on the radius token scale (md= 6px);xl(12px) restores the oldmdlook. SliderSkeletonis silent by default — passlabelto announce.diris no longer forced to"ltr": unset, the slider inherits the page's direction. Passdir="ltr"to keep a left-to-right slider on an RTL page.
Agent notes
Stable hooks for tooling and tests: data-component="slider" on the root, and
data-slot on every part — slider, slider-control, slider-track,
slider-fill, slider-thumb, slider-value, slider-tooltip,
slider-marks, slider-segment-gap, slider-origin, slider-label,
slider-min-label, slider-max-label, slider-leading-icon,
slider-trailing-icon. data-readonly marks the frozen state;
data-dragging (Base UI) marks an active drag.
Accessibility
Implements the WAI-ARIA Slider pattern (multi-thumb per the Slider
(Multi-Thumb) pattern) on Base UI's primitive — each thumb is a real
<input type="range">.
Keyboard
| Key | Action |
|---|---|
Tab / Shift+Tab | Move between thumbs (tab order never re-sorts by value) |
Arrow Right / Arrow Up | +step (Right/Left flip in RTL) |
Arrow Left / Arrow Down | −step |
Shift+Arrow | ±largeStep |
Page Up / Page Down | ±largeStep |
Home / End | Min / max (range: clamped to the neighbouring thumb ± minStepsBetweenValues) |
Screen readers
labelfeedsaria-labelledby; usegetAriaLabelfor distinct per-thumb names in range mode ("Minimum price", "Maximum price").aria-valuetextcomes fromformat/locale, orgetAriaValueTextfor word values — the rating example speaks "Okay" while showing it.- Mark labels are
aria-hiddenvisual anchors; the spoken value is thearia-valuetext, never the tick text. readOnlykeeps the thumb focusable and announced, so the value can still be read —disabledremoves it from the tab order entirely.
Touch
Every thumb has an invisible hit area centred on it: 44×44px on touch screens
and 24×24px with a mouse, so even the 12px xs thumb clears both floors. On
touch the rail itself is at least 44px tall too, and pressing it anywhere moves
the nearest thumb there. Continuous capsules are at least 24px tall at every
size.
- Two range thumbs closer than the hit area share it. The press goes to the
thumb moved last (at rest, the later one), so the other is reached by pressing
the rail beyond the overlap. When both ends must stay easy to grab on a phone,
keep them 44px apart with
minStepsBetweenValues. - At either end of the track the touch hit area reaches past the slider, by
half of what the thumb lacks of 44px: 12px for the default
mdthumb, 20px for abar. Against a scrolling edge on a phone, leave that much room beside the slider, or a thumb dragged to the end scrolls the page sideways.
Contrast
The fill inherits the shared intent tokens and the track is bg-secondary, and
three of those pairs do not clear the 3:1 that WCAG 1.4.11 asks of a
meaningful graphic. Measured by audit:contrast, both themes:
| Fill | Light | Dark |
|---|---|---|
none (primary) | 14.2:1 | 14.5:1 |
info | 4.1:1 | 5.7:1 |
error | 3.8:1 | 2.8:1 |
success | 2.6:1 | 6.8:1 |
warning | 1.7:1 | 8.9:1 |
That is the intent palette's own ceiling, accepted library-wide, not a Slider
tuning miss, and the thumb does not rescue it: on the
default discrete track it paints the same fill, and a continuous capsule's
default thumb is an invisible drag target. With valueDisplay defaulting to
"none", a slider in one of those intents should show its value in words:
"text", "tooltip-always" and, on a discrete track, "thumb" stay on screen,
while "tooltip" appears only while the thumb is hovered, dragged or focused.
AT matrix
| AT | Status |
|---|---|
| VoiceOver + Safari (macOS) | Passed 2026-09-14. Focus reads "40, Volume, slider" inside "Volume, group"; Right arrow reads "41", "42"; End reads "100". Transcript: tests/qa/vo-transcripts/slider.md |
| NVDA + Chrome (Windows) | UNTESTED — no Windows on the bench |
| JAWS | UNTESTED |
NVDA and JAWS stay untested until a Windows host exists.