Slider
Range slider from scratch. Single-thumb by default, two-thumb range with a second value, full keyboard + pointer support, three sizes, four colour variants, optional tick marks, and a value tooltip that can stay pinned or appear only while dragging. Vertical orientation supported.
Usage
import { Slider } from '@bwo-ui/react';
// Single thumb (controlled)
const [vol, setVol] = useState([40]);
<Slider value={vol} onValueChange={setVol} min={0} max={100} step={1} />
// Range (two thumbs)
const [range, setRange] = useState([20, 80]);
<Slider value={range} onValueChange={setRange} />
// Uncontrolled
<Slider defaultValue={[40]} onValueCommit={(v) => save(v)} />The slider operates on number[] — pass a single-element array for one thumb, a two-element array for a range. onValueChange fires continuously while dragging; onValueCommit fires once on release (use it for save / analytics so you're not spamming requests).
Sizes
sm (3 px track / 14 px thumb) for dense settings rows, md (5 / 20 px, default) for general use, lg (7 / 26 px) for hero positions where the slider is the primary interaction.
<Slider size="sm" value={[30]} onValueChange={…} />
<Slider value={[55]} onValueChange={…} /> {/* md */}
<Slider size="lg" value={[80]} onValueChange={…} />Variants
Four colour variants — primary (black), green, yellow, red. The variant colour drives the range fill and the thumb's focus ring, so the slider stays legible in either mode.
primary40green65yellow78red92<Slider variant="primary" value={[40]} onValueChange={…} />
<Slider variant="green" value={[65]} onValueChange={…} />
<Slider variant="yellow" value={[78]} onValueChange={…} />
<Slider variant="red" value={[92]} onValueChange={…} />Marks
Pass an array of numbers for unlabelled ticks, or an array of { value, label } objects to render labels under each tick. Marks within the active range highlight in the variant colour.
{/* Unlabelled ticks */}
<Slider min={1} max={5} step={1} marks={[1, 2, 3, 4, 5]} value={[3]} onValueChange={…} />
{/* Labelled ticks */}
<Slider
min={14}
max={30}
value={[22]}
onValueChange={…}
marks={[
{ value: 16, label: 'Cool' },
{ value: 20, label: '20°' },
{ value: 24, label: '24°' },
{ value: 28, label: 'Warm' },
]}
/>Tooltip
Show a small bubble above the thumb with the current value. Three modes: never (default — no tooltip), drag (appears while dragging or focused — keyboard-friendly), always (pinned — for dashboards where the value should always be visible).
Use the formatValue prop to control the display string. The same formatter feeds aria-valuetext on each thumb so screen readers announce the formatted value, not the raw number.
<Slider tooltip="drag" formatValue={(v) => `${v}%`} value={vol} onValueChange={…} />
<Slider
tooltip="always"
variant="green"
formatValue={(v) => `€${v.toFixed(0)}`}
value={price}
onValueChange={…}
/>Step + largeStep
step sets the increment. largeStep sets the larger jump used by Shift+Arrow / PageUp / PageDown. Home / End jump to the min / max bounds.
Tab to focus the thumb. Arrow keys step by step. Shift+Arrow or PageUp / PageDown step by largeStep. Home / End jump to the bounds.
<Slider
value={[50]}
onValueChange={…}
min={0}
max={100}
step={5}
largeStep={20}
marks={[0, 25, 50, 75, 100]}
tooltip="drag"
/>Vertical orientation
Pass orientation="vertical" for a column-axis slider. The track measures 160 px tall by default — wrap in a parent with the height you need if that doesn't suit. The keyboard mapping mirrors horizontal: Up = increase, Down = decrease.
<Slider
orientation="vertical"
variant="green"
tooltip="drag"
value={mid}
onValueChange={setMid}
/>Disabled
disabled removes pointer events and reduces opacity. The thumb is tabIndex=-1 so it's skipped by keyboard navigation.
<Slider value={[40]} disabled />
<Slider value={[20, 70]} variant="green" disabled />Recipe — volume with mute
Classic mute-button pattern. The button toggles a muted flag; the slider renders [0] while muted and is disabled. Dragging the slider while muted un-mutes (so the user can recover with a single gesture).
function VolumeWithMute() {
const [volume, setVolume] = useState([72]);
const [muted, setMuted] = useState(false);
return (
<>
<button onClick={() => setMuted((m) => !m)}>
{muted ? 'Unmute' : 'Mute'}
</button>
<Slider
value={muted ? [0] : volume}
onValueChange={(v) => {
setVolume(v);
if (muted && v[0]! > 0) setMuted(false);
}}
disabled={muted}
/>
</>
);
}Form integration
Pass name to render a hidden <input> with each thumb's value — Slider submits cleanly inside a native form. For a range slider, the inputs are emitted as name="range[0]" / name="range[1]" so PHP / Rails-style array decoders pick them up.
<form action="/save" method="post">
<Slider name="volume" defaultValue={[40]} />
{/* Single thumb → <input type="hidden" name="volume" value="40" /> */}
<Slider name="range" defaultValue={[20, 80]} />
{/* Range → name="range[0]" and name="range[1]" */}
<button type="submit">Save</button>
</form>Accessibility
- Each thumb is a
role="slider"witharia-valuemin/aria-valuemax/aria-valuenowwired automatically. - Pass
aria-labelon the Slider so the thumbs inherit a descriptive name ("Volume", "Price range") instead of the default "Value 1". formatValuealso populatesaria-valuetexton each thumb — screen readers announce "€42" instead of just "42" when you provide a formatter.- Keyboard support:
←/→(or↓/↑) step bystep;Shift+ arrow orPageUp/PageDownstep bylargeStep;Home/Endjump to min / max. - On a range slider, each thumb is bounded by the others — moving the lower thumb above the upper's value is prevented automatically.
- Tooltip in
dragmode also shows on keyboard focus so users navigating with Tab see the value just like mouse users.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number[] | — | Controlled value. Pass a 1-element array for a single thumb, 2-element for a range. |
defaultValue | number[] | [50] | Uncontrolled initial value. |
onValueChange | (value: number[]) => void | — | Fires on every change while dragging or stepping. Use this for live UI updates. |
onValueCommit | (value: number[]) => void | — | Fires once when the user releases the thumb or hits a key bound (Enter, Home, End). Use for save / analytics. |
min | number | 0 | Minimum value. |
max | number | 100 | Maximum value. |
step | number | 1 | Step granularity. |
largeStep | number | — | Larger step for Shift+Arrow / Page keys. Defaults to `step × 10`. |
size | 'sm' | 'md' | 'lg' | 'md' | 3 / 14, 5 / 20, 7 / 26 px (track / thumb). |
variant | 'primary' | 'green' | 'yellow' | 'red' | 'primary' | Range fill + thumb ring colour. |
marks | SliderMark[] | — | Tick marks. Pass numbers (`[1, 2, 3]`) for unlabelled ticks, or `{ value, label }` objects to render labels under each tick. |
tooltip | 'never' | 'drag' | 'always' | 'never' | `drag` shows the tooltip while dragging or focused; `always` keeps it pinned. |
formatValue | (value: number) => ReactNode | — | Formatter for the tooltip and `aria-valuetext`. Default: `String(value)`. |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Vertical sliders default to 160 px tall — set a parent height to override. |
inverted | boolean | — | Reverse the value direction (right-to-left horizontal, top-to-bottom vertical). |
disabled | boolean | — | Removes pointer events, reduces opacity, sets `tabIndex={-1}` on thumbs. |
name | string | — | When set, renders hidden inputs alongside each thumb so the slider submits with native forms. |
aria-label | string | — | Passed to each thumb. Without it, thumbs get "Value 1" / "Value 2" as a fallback. |
…rest | HTMLAttributes<HTMLSpanElement> | — | All native span attributes are forwarded — `style`, `className`, `aria-*`. |