Slider
Slider picks a value, or a start and end value, by dragging along a track. Use it for settings where the exact number matters less than the feel, such as a study goal, playback speed or a range filter.
Beta until design confirms the code-only parts (sm size, hover, focus, disabled, ticks, mark labels under the track, vertical) and the darker rail and thumb edges (State/Faded/Base, 4.23:1; Figma's Sub-300 edge was 1.53:1). Screen reader testing with VoiceOver (iOS and macOS), NVDA and TalkBack is still to do.
Preview
Change the controls to try every supported combination. Drag a thumb, press the track, or tab to a thumb and use the arrow keys.
const [value, setValue] = useState(25)
<Slider
label="Daily study goal"
max={60}
step={5}
value={value}
onValueChange={setValue}
formatValue={(v) => `${v} min`}
showValue
tooltip="auto"
/>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages and aren't published yet (D4, D12). Inside the Nuton monorepo, add them as workspace dependencies.
pnpm add @nuton/ui @nuton/tokensnpm install @nuton/ui @nuton/tokensyarn add @nuton/ui @nuton/tokensimport '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'Usage
import { RangeSlider, Slider } from '@nuton/ui'
<Slider
label="Daily study goal"
min={10}
max={120}
step={5}
value={goal}
onValueChange={setGoal}
onValueCommit={saveGoal}
showValue
formatValue={(v) => `${v} min`}
/>
<RangeSlider label="Lesson length" max={60} step={5} value={range} onValueChange={setRange} />- Use
Sliderfor one value andRangeSliderfor a start and end. Each thumb is a native<input type="range">, so forms, mobile screen readers and the keyboard work without extra code. - Use a slider when an approximate value is fine. When people need an exact number, use an Input (you can pair both).
- Save on
onValueCommit(pointer released or key pressed), not on everyonValueChangewhile dragging. - Give every slider a
label, a Field, oraria-label. UsegetValueTextwhen the number alone doesn't say enough (“1.5 times speed”, “Focused, 3 of 4”). - Keep mark labels short. They are hidden from screen readers, so say the same thing in
getValueText.
Variants and sizes
Sizes
<Slider label="Medium (default)" defaultValue={40} />
<Slider label="Small" size="sm" defaultValue={40} />| Part | md (Figma) | sm (code-only) |
|---|---|---|
| Track | 10 tall, radius full, 1px border | 6 tall |
| Thumb | 30 circle, toggle-switch shadow | 20 circle |
| Hit area | 30 tall, the whole track | 24 tall, the whole track |
| Label row | Label/X Small, Text/Sub-600; value at the end; 8px above the track | |
| Mark labels | Label/Small, Text/Strong-950 | Label/X Small |
| Value bubble | Tooltip 2X-Small, dark: 20 tall, 4px tail, 4px above the thumb | |
Range (two thumbs)
<RangeSlider label="Lesson length" max={60} step={5} defaultValue={[10, 30]} showValue />Marks, formatted values and the value bubble
Marks add ticks on the track and optional labels under it. formatValue sets the visible text; getValueText sets what screen readers say.
<Slider
label="Playback speed"
min={0.5}
max={2}
step={0.25}
defaultValue={1}
showValue
formatValue={(v) => `${v}x`}
getValueText={(v) => `${v} times speed`}
marks={[{ value: 0.5, label: '0.5x' }, { value: 1, label: '1x' }, { value: 1.5, label: '1.5x' }, { value: 2, label: '2x' }]}
/><Slider aria-label="Volume" defaultValue={60} tooltip="always" />Vertical
Vertical sliders grow upwards. Set the length with --nu-slider-length (default 200px). Code-only: Figma has no vertical slider.
<Slider
aria-label="Text size"
orientation="vertical"
min={12}
max={24}
step={2}
defaultValue={16}
tooltip="auto"
formatValue={(v) => `${v}px`}
marks={[{ value: 12, label: 'Small' }, { value: 24, label: 'Large' }]}
/>States and interactions
tooltip="auto" the value bubble shows.State tokens
Set on .nu-slider. Override them on a wrapper class to restyle a slider, as the Audio player does for its seek bar.
| Token | Value | Used for |
|---|---|---|
--nu-slider-track-size · --nu-slider-thumb-size · --nu-slider-hit | 10 · 30 · 24 (sm 6 · 20) | Geometry |
--nu-slider-rail · --nu-slider-rail-border | Bg/Soft-200 · Stroke/Sub-300 | Track |
--nu-slider-fill | Bg/Strong-950 | Filled part |
--nu-slider-thumb-bg · --nu-slider-thumb-border · --nu-slider-thumb-shadow | Bg/White · Stroke/Sub-300 · toggle-switch | Thumb |
--nu-slider-thumb-border-hover | State/Faded/Base | Thumb, hover and drag |
--nu-slider-tick · --nu-slider-tick-active | State/Faded/Base · Bg/White | Ticks off and on the fill |
--nu-slider-*-disabled | Weak-50, Soft-200, Sub-300, Disabled-300 | Disabled |
--nu-slider-length | 200px | Vertical length |
Differences from Figma (pending a decision)
- Thumb edge: Stroke/Sub-300, as on the live pricing slider. Figma's Soft-200 edge is 1.19:1 against white.
- Track: Figma's rail and thumb edge (Stroke/Sub-300) is 1.53:1, below the 3:1 a control boundary needs (WCAG 1.4.11). Code uses State/Faded/Base (4.23:1), the same rule as Switch and Checkbox (C48, C54); hover and drag step to Icon/Sub-600.
- Figma's tick labels sit above the track; code puts them under it, because the value bubble uses the space above.
Composition examples
Study settings
Each slider saves when the change is finished and confirms it in a status message. The difficulty slider uses words for its value.
Range filter
Inside a Field
Audio player seek bar
The Audio player seek bar is a Slider with 5-second arrow steps, 1-minute Page steps and spoken times, restyled through the tokens above.
API reference
Slider props
| Prop | Type | Default | Description |
|---|---|---|---|
value · defaultValue | number | min | Controlled or uncontrolled value. Values outside min–max are clamped. |
onValueChange | (value: number) => void | — | Called as the value changes: drag, track press, keys or assistive technology. |
onValueCommit | (value: number) => void | — | Called when a change is finished: pointer released or a key pressed. Save here. |
getValueText | (value: number) => string | — | The aria-valuetext. Default formatValue when set, otherwise the number. |
id | string | — | Id of the native input, for an external <label htmlFor>. Inside a Field, the Field sets it. |
inputRef | Ref<HTMLInputElement> | — | Ref to the native input (the forwarded ref is the root element). |
RangeSlider props
| Prop | Type | Default | Description |
|---|---|---|---|
value · defaultValue | [number, number] | [min, max] | Start and end values. |
onValueChange · onValueCommit | (value: [number, number]) => void | — | As for Slider, with both values. |
minStepsBetweenThumbs | number | 0 | Smallest gap between the thumbs, in steps. The thumbs never cross. |
getValueText | (value: number, thumb: 0 | 1) => string | — | Each thumb's aria-valuetext. |
labels | Partial<SliderLabels> | SLIDER_LABELS | Thumb names start (“Minimum”) and end (“Maximum”), read after the label, and the visible separator (“ – ”). |
Shared props
| Prop | Type | Default | Description |
|---|---|---|---|
min · max · step | number | 0 · 100 · 1 | Range and grid. Values snap to the step, counted from min. |
keyStep | number | step | Arrow-key change. |
pageStep | number | a tenth of the range | Page Up / Page Down change. |
marks | Array<number | { value: number; label?: ReactNode }> | — | Ticks on the track and optional labels under it. Labels are hidden from screen readers. |
label | ReactNode | — | Visible label above the track (a <label> for Slider; names the group for RangeSlider). |
description | ReactNode | — | Text under the slider, linked with aria-describedby. |
showValue | boolean | false | Shows the formatted value at the end of the label row (hidden from screen readers; the value text carries it). |
tooltip | 'never' | 'auto' | 'always' | 'never' | Value bubble above each thumb. auto shows it on hover, keyboard focus and drag. |
formatValue | (value: number) => string | the number | Visible value text in the label row and bubble. |
size | 'sm' | 'md' | 'md' | md = Figma (10px track, 30px thumb); sm = 6px track, 20px thumb. |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Vertical sliders grow upwards. |
disabled | boolean | false | Disables the native inputs. Inside a Field, this comes from the Field. |
name · form | string | — | Form attributes for the native input(s). RangeSlider submits two values under one name. |
aria-label · aria-labelledby · aria-describedby | string | — | Name and description when there is no visible label. For RangeSlider, each thumb adds its own name. |
className · …div props | HTMLAttributes<HTMLDivElement> | — | Go to the root element; the forwarded ref points at it. |
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves to each thumb in turn |
| → / ↑ | Increases by keyStep (← in right-to-left pages) |
| ← / ↓ | Decreases by keyStep (→ in right-to-left pages) |
| Page Up / Page Down | Changes by pageStep |
| Home / End | Minimum or maximum (for a range thumb, as far as the other thumb allows) |
Screen readers
Each thumb is a native range input, announced as “slider” with its name, value and aria-valuetext. Range thumbs are named “Lesson length Minimum” and “Lesson length Maximum”, inside a named group, and each one's min or max follows the other thumb. Native inputs also answer mobile swipe gestures. The value row, bubble and mark labels are hidden so they aren't read twice.
Focus
Focus stays on the native input; the thumb shows a 2px primary outline. Pressing the track focuses the thumb it moved, without a focus ring.
Contrast and colour
Fill 17.93:1 against white and 15.04:1 against the rail. The thumb position and fill length show the value, never colour alone. The rail and thumb edges are 4.23:1 (State/Faded/Base). Label and value text Sub-600, 6.69:1. In forced-colours mode the track, fill and thumbs use system colours.
Touch and pointer
The whole track is a target, at least 24px tall (WCAG 2.5.8); every thumb is at least 24 × 24. Pressing the track jumps the nearest thumb; grabbing a thumb doesn't move it. Vertical page scrolling still works over a horizontal slider. Transitions stop with reduced motion.
You must: name every slider, give a spoken value with getValueText when the number isn't meaningful, and offer a text input where an exact value matters.
Figma mapping
| Figma | Code |
|---|---|
| Text row: Label, (Optional), value | label (pass “(Optional)” inside it), showValue |
| Slider: BG, Progress, thumb | .nu-slider__rail, __fill, __thumb |
| Dot with Tooltip [1.1] | tooltip |
| Tick labels (1k … 50k) | marks |
| (none) | Range, sizes, vertical, states and keyboard are code-only |
Guidelines and related components
Use Slider for
- Settings where a rough value is fine (goal, speed, volume, text size)
- Filtering by a range (lesson length, date range)
- Seeking in audio or video
Don't use Slider for
- Exact numbers, such as a score or an age (use an Input)
- A few named choices with no order (use Radio or a Segmented control)
- Showing progress (use Progress)