Slider
Slider picks a number — or a [min, max] range — from a continuous scale. It
is a standalone component (no upstream Spar primitive), so the wrapper owns the
value math, the pointer dragging, and the full keyboard and ARIA surface. Each
thumb is its own role="slider" element, which is what assistive technology
reads and what the arrow keys drive.
Usage
import { Slider } from '@takeoff-ui/react-spar';
<Slider defaultValue={40} />
Compose it inside a Field to wire the visible label, the description, and the
shared disabled / readOnly / invalid / required state automatically.
Playground
Range
Set range to render two thumbs. The value becomes a [min, max] tuple, and
onValueChange reports it in the same shape. Dragging one handle past the other
swaps them, so the committed tuple always stays ascending.
Multiple handles
The number of thumbs is decided by the value array, not by a separate prop: pass
three entries and three handles render, each bounded by its neighbours.
onValueChange reports the whole array, ordered ascending.
The outer handles keep the data-thumb="min" / "max" styling hooks; a handle
in between is neither, so it carries no data-thumb at all. Accessible names
follow the same logic — a two-handle range reads as Minimum / Maximum, while
three or more fall back to Value 1, Value 2, … which consumers can override
per thumb with aria-label.
Minimum distance
Set minDistance to keep a gap between adjacent range handles. With a positive
gap the handles can no longer cross — dragging one into another stops it against
its neighbour instead of swapping. Seed the initial values at least this far
apart; they are not reshaped.
Sync with an input
The slider is controlled like any input — bind value / onValueChange to
state and pair it with a number field for exact entry. Typing moves the thumb;
dragging updates the field.
Change events
onValueChange fires on every committed change while interacting — each drag
frame, keystroke, or track press. onValueChangeEnd fires once when the
interaction settles (pointer release, or a single keystroke), with the final
value — use it when only the settled value matters, e.g. to persist or refetch.
Value tooltip
The value bubble follows the handle on drag or keyboard focus by default
(tooltip="auto"). Set tooltip="always" to pin it open, or tooltip="never"
to hide it entirely — useful when a Slider.Value already surfaces the number.
A disabled slider hides the bubble either way.
The bubble is a CSS node parented to the handle rather than a portaled overlay,
so an ancestor with overflow: hidden or overflow: auto clips it — for
example a slider near the top of a scrollable dialog body or inside a table
cell. In an overflow container, prefer Slider.Value (which stays in flow) or
leave tooltip="auto" so the bubble only appears transiently on drag/focus.
Ticks
The default anatomy is the track alone. To mark the step grid below it, compose
Slider.Ticks after Slider.Track — an indicator is anatomy, so it is added by
composition rather than switched on by a prop.
Track fill
track sets how the rail fills. normal (default) fills from the start to the
thumb (or between a range's handles); inverted fills the complement — from the
thumb to the end for a single slider, and outside the two handles for a range;
none drops the fill entirely, leaving just the rail and the thumbs.
Orientation
Set orientation="vertical" to run the rail bottom-to-top: the bottom edge is
min, dragging upward increases the value, and the keyboard behaves the same
(Up/Right increase, Down/Left decrease). Every thumb reports the axis through
aria-orientation.
A vertical rail has no intrinsic length — a horizontal one takes its width from the parent, but nothing gives a vertical one a height. It fills its container, so give the slider (or a wrapping element) a height:
.tk-slider[data-orientation='vertical'] {
height: 240px;
}
Sizes
Variants
States
Custom styling
Every part accepts className, classNames, style, and slotProps, so the
whole slider is yours to re-skin without leaving the composition — the rail
(Slider.Track), the fill (Slider.Range), and the handle (Slider.Thumb,
whose slots are the root handle, the tooltip bubble, and the arrow
pointer), plus the bubble's content through a Slider.Thumb child. The bubble
and its arrow share the --tk-slider-tooltip-bg custom property, so one
override recolours both, while the arrow keeps its own --tk-slider-arrow-width
/ -height. The fill gradient is pinned to the full track width with
background-size so it maps to the absolute scale rather than stretching across
the current fill — a low value stays blue instead of squeezing the whole
spectrum into a sliver. The tooltip is revealed on drag or keyboard focus, so
grab the handle to see it.
Keyboard
| Key | Action |
|---|---|
| ← ↓ | Decrease the focused thumb by one step |
| → ↑ | Increase the focused thumb by one step |
| Page Down | Decrease by ten steps |
| Page Up | Increase by ten steps |
| Home | Jump to the lowest value the thumb may take |
| End | Jump to the highest value the thumb may take |
In a range slider each thumb is bounded by its neighbour, so the keyboard can never push one handle past the other.
API Reference
Slider
Props
| Name | Type | Default | Description |
|---|---|---|---|
| range | boolean | false | Renders a multi-handle slider: the value becomes an array and one thumb renders per entry (two by default). Dragging one handle past another swaps them, so the committed array stays ascending. |
| value | number | number[] | - | Controlled value — a number by default, an array when range is set (one thumb per entry; fewer than two entries fall back to [min, max]). Each entry is clamped into [min, max], snapped to step, and the array is ordered ascending. Takes precedence over defaultValue, which is ignored when both are passed. |
| defaultValue | number | number[] | min | Initial value for uncontrolled usage — a number by default, an array when range is set (one thumb per entry). Defaults to min for a single slider and [min, max] for a range. Controlled/uncontrolled mode is latched on the first render. |
| min | number | 0 | Lowest selectable value. Non-finite values fall back to the default. |
| max | number | 100 | Highest selectable value. Non-finite values and values at or below min fall back to min + 100 (with a dev-only console warning, since an inverted range is a consumer bug). |
| step | number | 1 | Granularity the value snaps to, counted from min. Non-finite and non-positive values fall back to the default. |
| minDistance | number | 0 | Minimum gap kept between adjacent thumbs of a range, in value units. With a positive gap the handles can no longer cross — dragging one into another stops it against its neighbour instead of swapping. Ignored by a single slider; initial values are not reshaped, so seed them at least this far apart. |
| disabled | boolean | false | Blocks interaction and mutes the fill. Inherits the surrounding Field's disabled state when composed inside one. |
| readOnly | boolean | false | Renders the value but blocks every value-changing interaction. Inherits the surrounding Field's read-only state when composed inside one. |
| required | boolean | false | Marks the slider as required for form validation. Inherits the surrounding Field's required state when composed inside one. |
| invalid | boolean | false | Applies the invalid treatment and sets aria-invalid on every thumb. Inherits the surrounding Field's invalid state when composed inside one. |
| orientation | SliderOrientation | 'horizontal' | Axis the rail runs along. A vertical rail runs bottom-to-top and fills its container's height (as a horizontal rail fills its width), so give the parent a height. |
| size | SliderSize | 'base' | Visual scale — changes track thickness and thumb diameter. |
| variant | SliderVariant | 'primary' | Fill color variant. |
| track | SliderTrackMode | 'normal' | How the rail fill renders. normal fills from the start to the thumb (or between a range's thumbs); inverted fills the complement instead — from the thumb to the end for a single slider, and outside the two handles for a range; none drops the fill entirely, leaving just the rail and the thumbs. |
| tooltip | SliderTooltip | 'auto' | When the drag value bubble is shown. auto reveals it while the handle is dragged or keyboard-focused; always pins it open; never hides it entirely — handy when a Slider.Value already surfaces the number. A disabled slider hides the bubble regardless. |
| formatValue | (value: number) => string | - | Formats a value for every readout — the drag tooltip, Slider.Value, and aria-valuetext. Returns a string, so it formats the value rather than overriding the rendered node; use Slider.Thumb / Slider.Value children for richer content. When omitted, the raw number is shown and aria-valuetext is dropped. |
| name | string | - | Name submitted with the form. A single slider submits one entry under this exact name; a two-handle range submits -min / -max, and a range with more handles submits each value under a 1-based -1, -2, … suffix so no handle is dropped. |
| form | string | - | id of the form the hidden inputs belong to. |
| children | React.ReactNode | - | Optional anatomy override. When omitted, Slider renders Slider.Track wrapping Slider.Range and one thumb per value. |
| classNames | Partial<Record<"root", string>> | - | Per-slot class name overrides. |
| slotProps | Partial<Record<"root", HTMLAttributes<HTMLElement>>> | - | Per-slot HTML attribute overrides. |
| className | string | - | Appends custom classes to the root slot. |
Events
| Name | Type | Default | Description |
|---|---|---|---|
| onValueChange | (value: number | number[]) => void | - | Fired on every committed value change while interacting — each drag frame, keystroke, or track press. Receives a number by default and the full ascending array when range is set. |
| onValueChangeEnd | (value: number | number[]) => void | - | Fired once at the end of an interaction with the final value: on pointer release after a drag or track press, and once per committed keystroke. Use it when only the settled value matters, while onValueChange streams the live value. |
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| data-size | Always | Reflects the resolved size prop for theme recipe scoping (rail thickness and thumb diameter). |
| data-variant | Always | Reflects the resolved variant prop that drives the fill color. |
| data-orientation | Always | Reflects the resolved orientation prop. A vertical rail runs bottom-to-top and takes its height from its container, as a horizontal rail takes its width. |
| data-tooltip | Always | Reflects the resolved tooltip prop (auto | always | never) that controls when the value bubble shows. |
| data-track | Always | Reflects the resolved track prop (normal | inverted | none) that sets the rail fill mode; inverted fills the complement of the selection, none drops the fill but keeps the rail. |
| data-range | When range is set | Marks a multi-thumb slider (two or more handles) so the recipe can style the fill as a band between the outermost handles. |
| data-disabled | When disabled (own prop or inherited from a surrounding Field) | Mutes the fill and blocks every interaction. |
| data-readonly | When read-only (own prop or inherited from a surrounding Field) | Renders the value while blocking value-changing interaction. |
| data-invalid | When invalid (own prop or inherited from a surrounding Field) | Applies the invalid treatment; each thumb also exposes aria-invalid. |
| data-required | When required (own prop or inherited from a surrounding Field) | Marks the control as required; each thumb also exposes aria-required. |
| data-thumb | On each thumb of a range slider | min on the first handle and max on the last. A handle between them is neither end, so it carries no data-thumb rather than a misleading one. |
| data-dragging | On the thumb the pointer currently controls | Reveals the value tooltip and suppresses the position transition so the handle tracks the pointer exactly. |
| data-focus | On the focused thumb | Draws the focus ring and reveals the value tooltip for keyboard users. |
| role="slider" | On every thumb | The thumb is the accessibility owner: it carries aria-valuenow / aria-valuemin / aria-valuemax and the keyboard surface. |
| role="group" | When range is set | Ties the two role="slider" thumbs together under the surrounding Field label. |
Slider.Track
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| data-orientation | Inherited from the root (descendant selector) | A vertical rail flips its long axis; the track reads the root’s data-orientation rather than carrying its own. |
Slider.Range
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| data-track | Inherited from the root (descendant selector) | The inverted / none fill mode is read from the root’s data-track; the band recolours or hides itself accordingly. Its offset and length are written inline (a continuous value is not a data-* hook). |
Slider.Thumb
Props
| Name | Type | Default | Description |
|---|---|---|---|
| index | number | 0 | Which value this thumb controls. The default anatomy renders index 0 (and 1 for a range); pass it explicitly only when composing the thumbs by hand. |
| disabled | boolean | false | Disables just this handle — it cannot be moved and is skipped as a drag target, while the other thumbs stay interactive. A neighbour dragged into a disabled handle stops against it. The slider’s own disabled still disables every thumb. |
| children | React.ReactNode | ((state: SliderThumbRenderProps) => React.ReactNode) | - | Content of the value bubble. When omitted, the formatted value renders. A plain node replaces it with static content; a function receives this thumb’s value / formatted / index / isDragging / isFocused. Either form swaps only what the bubble shows — the handle and bubble chrome stay the thumb’s. |
| classNames | Partial<Record<SliderThumbSlot, string>> | - | Per-slot class name overrides. |
| slotProps | Partial<Record<SliderThumbSlot, HTMLAttributes<HTMLElement>>> | - | Per-slot HTML attribute overrides. |
| className | string | - | Appends custom classes to the root slot of this part. |
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| data-slot="tooltip" | Always | The value bubble parented to the handle; style or override it via classNames.tooltip / slotProps.tooltip. |
| data-slot="arrow" | Always | The bubble’s pointer, a real element (not a pseudo-element) so classNames.arrow / slotProps.arrow can resize or recolour it. |
| data-thumb | On the first / last handle of a range | min on the first handle and max on the last; a middle handle carries none. |
| data-dragging | While the pointer controls this handle | Reveals the value bubble and suppresses the position transition so the handle tracks the pointer exactly. |
| data-focus | While this handle holds keyboard focus | Draws the focus ring and reveals the value bubble for keyboard users. |
| data-disabled | When this handle is disabled (own prop or the whole slider) | Mutes just this handle and hides its value bubble. |
| role="slider" | Always | The handle is the accessibility owner: it carries aria-valuenow / aria-valuemin / aria-valuemax and the keyboard surface. |
Slider.Ticks
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| data-slot="tick" | On each step mark | One mark of the grid; style or override it via classNames.tick / slotProps.tick. Positions are written inline. Rendered aria-hidden (decorative). |
Slider.Value
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Type Definitions
| Name | Definition |
|---|---|
| SliderOrientation | 'horizontal' | 'vertical' |
| SliderSize | 'small' | 'base' | 'large' |
| SliderVariant | 'primary' | 'info' | 'success' | 'danger' | 'warning' |
| SliderTrackMode | 'normal' | 'inverted' | 'none' |
| SliderTooltip | 'auto' | 'always' | 'never' |
| SliderThumbRenderProps | { value: number; formatted: string; index: number; isDragging: boolean; isFocused: boolean } |
| SliderThumbSlot | 'root' | 'tooltip' | 'arrow' |
| SliderTicksSlot | 'root' | 'tick' |
| SliderValueRenderProps | { values: number[]; formatted: string[]; range: boolean } |