Slider
A Radix-style slider. The value is a number[] — one entry per thumb — paired with onValueChange. [v] renders a single-thumb bar that paints from min to v; [lo, hi] renders a two-thumb range with the fill between them. Distinct from LabeledSlider (single-value primitive with a built-in label + readout) and RangeInput (a strict { lo, hi } pair under the min <= lo <= hi <= max invariant).
Volume42%
Bracket20 – 80
Installation
npx shadcn@latest add https://craftbits.dev/r/slider.jsonUsage
Single-thumb — pair value with onValueChange. The fill grows from min to the thumb:
import { Slider } from "@craft-bits/core";
const [volume, setVolume] = useState<number[]>([42]);
<Slider
min={0}
max={100}
step={1}
value={volume}
onValueChange={setVolume}
aria-label="Volume"
/>Two-thumb — same primitive, just two entries in the array. The fill paints between the two thumbs:
const [bracket, setBracket] = useState<number[]>([20, 80]);
<Slider
min={0}
max={100}
value={bracket}
onValueChange={setBracket}
aria-label="Bracket"
/>Uncontrolled — pass defaultValue alone and let the component own its array:
<Slider min={0} max={100} defaultValue={[35]} aria-label="Brightness" />Understanding the component
- Radix-style contract.
value: number[]+onValueChange: (value: number[]) => voidmirrors@radix-ui/react-slider'sRootAPI, so callers migrating off Radix (or off the source project'sradix-uiumbrella) can swap the import without re-wiring state. - Thumb count is the array length.
[v]is a one-thumb bar;[lo, hi]is a two-thumb range;N > 2is supported and the painted fill spans from the lowest to the highest entry. - N native ranges, one track. Each thumb is a real
<input type="range">layered over the shared rail. Native arrow-key stepping, Home / End, PageUp / PageDown, and screen-readerrole="slider"semantics ship for free. - Independent thumbs even when overlapping. Pointer-events are disabled on the underlying inputs and re-enabled on the thumbs alone, so adjacent thumbs stay individually grabbable when their values coincide.
- CSS-variable painted fill. The highlighted segment is a sibling div whose
left/rightare CSS-variable–driven percentages of the lowest / highest entry. No JS layout measurement. - Controlled + uncontrolled. Pass
valuefor controlled use, ordefaultValuealone for a self-owned array. Missing keys fall back to[min]. - Token-only theming. Track, fill, thumb, halo, and focus ring all read from
--cb-*tokens.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
min | number | 0 | Inclusive lower bound of the slider domain. |
max | number | 100 | Inclusive upper bound of the slider domain. |
step | number | 1 | Step granularity for every thumb. |
value | readonly number[] | — | Controlled value array. One entry per thumb. Pair with onValueChange. |
defaultValue | readonly number[] | [min] | Uncontrolled initial array. |
onValueChange | (value: number[]) => void | — | Fired with the next array whenever any thumb moves. Entries are clamped into [min, max]. |
disabled | boolean | false | Disables every thumb. |
aria-label | string | "Value" | Accessible name applied to each thumb. |
aria-labelledby | string | — | Overrides aria-label per the ARIA precedence rules. |
className | string | — | Merged onto the outer wrapper. |
Accessibility
- Each thumb is a real
<input type="range">—role="slider", arrow-key stepping, Home / End jump to bounds, PageUp / PageDown step in larger increments. All browser-native. - The wrapper declares
role="group"so screen readers announce the thumbs as a related set.aria-labelledbywins when provided, otherwisearia-labelfalls through. - Each thumb mirrors
aria-valuemin,aria-valuemax, andaria-valuenowfrom React state. - The painted fill and rail are wrapped in
aria-hidden="true"— the slider roles already announce the values. - All thumbs stay keyboard-reachable in DOM order even when their values coincide.
- Focus is visible via a token-driven ring around each thumb (
--cb-accent-mutedhalo).
Credits
- Extracted from:
algoflashcards(src/platform/ui/slider.tsx). The source was a thin wrapper aroundradix-ui'sSliderPrimitive.Root. craft-bits drops theradix-uiumbrella dependency, rebuilds the same Radix-style contract (value: number[]+onValueChange) on N overlaid native<input type="range">elements, and re-skins the chrome onto the shared--cb-*token system. Distinct fromLabeledSlider(single-value with built-in label + unit readout) andRangeInput(strict{ lo, hi }pair).