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
  • Accessibility
  • Figma Slider [1.1] 3636:41047 (pricing block 3636:41038)
  • packages/ui/src/components/Slider

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.

tsx
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.

bash
pnpm add @nuton/ui @nuton/tokens
tsx
import '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'

Usage

tsx
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 Slider for one value and RangeSlider for 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 every onValueChange while dragging.
  • Give every slider a label, a Field, or aria-label. Use getValueText when 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

Partmd (Figma)sm (code-only)
Track10 tall, radius full, 1px border6 tall
Thumb30 circle, toggle-switch shadow20 circle
Hit area30 tall, the whole track24 tall, the whole track
Label rowLabel/X Small, Text/Sub-600; value at the end; 8px above the track
Mark labelsLabel/Small, Text/Strong-950Label/X Small
Value bubbleTooltip 2X-Small, dark: 20 tall, 4px tail, 4px above the thumb

Range (two thumbs)

Lesson length

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.

Vertical

Vertical sliders grow upwards. Set the length with --nu-slider-length (default 200px). Code-only: Figma has no vertical slider.

States and interactions

DefaultBg/Soft-200 rail with a Stroke/Sub-300 edge, Bg/Strong-950 fill, white thumb.
Hover and drag (code-only)The thumb edge darkens to State/Faded/Base; with tooltip="auto" the value bubble shows.
Focus (code-only)Press Tab: a 2px primary outline around the thumb. Figma has no focus state.
Disabled (code-only)Weak-50 rail, Sub-300 fill, no shadow, Disabled-500 text. Not focusable.

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.

TokenValueUsed for
--nu-slider-track-size · --nu-slider-thumb-size · --nu-slider-hit10 · 30 · 24 (sm 6 · 20)Geometry
--nu-slider-rail · --nu-slider-rail-borderBg/Soft-200 · Stroke/Sub-300Track
--nu-slider-fillBg/Strong-950Filled part
--nu-slider-thumb-bg · --nu-slider-thumb-border · --nu-slider-thumb-shadowBg/White · Stroke/Sub-300 · toggle-switchThumb
--nu-slider-thumb-border-hoverState/Faded/BaseThumb, hover and drag
--nu-slider-tick · --nu-slider-tick-activeState/Faded/Base · Bg/WhiteTicks off and on the fill
--nu-slider-*-disabledWeak-50, Soft-200, Sub-300, Disabled-300Disabled
--nu-slider-length200pxVertical 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.

We'll remind you when you're close.

Range filter

Lesson length
  • Photosynthesis15 min
  • Respiration22 min

2 of 5 lessons

Inside a Field

Applies to the front and back of every card.

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

Slider props
PropTypeDefaultDescription
value · defaultValuenumberminControlled 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.
idstring—Id of the native input, for an external <label htmlFor>. Inside a Field, the Field sets it.
inputRefRef<HTMLInputElement>—Ref to the native input (the forwarded ref is the root element).

RangeSlider props

RangeSlider props
PropTypeDefaultDescription
value · defaultValue[number, number][min, max]Start and end values.
onValueChange · onValueCommit(value: [number, number]) => void—As for Slider, with both values.
minStepsBetweenThumbsnumber0Smallest gap between the thumbs, in steps. The thumbs never cross.
getValueText(value: number, thumb: 0 | 1) => string—Each thumb's aria-valuetext.
labelsPartial<SliderLabels>SLIDER_LABELSThumb names start (“Minimum”) and end (“Maximum”), read after the label, and the visible separator (“ – ”).

Shared props

Props shared by Slider and RangeSlider
PropTypeDefaultDescription
min · max · stepnumber0 · 100 · 1Range and grid. Values snap to the step, counted from min.
keyStepnumberstepArrow-key change.
pageStepnumbera tenth of the rangePage Up / Page Down change.
marksArray<number | { value: number; label?: ReactNode }>—Ticks on the track and optional labels under it. Labels are hidden from screen readers.
labelReactNode—Visible label above the track (a <label> for Slider; names the group for RangeSlider).
descriptionReactNode—Text under the slider, linked with aria-describedby.
showValuebooleanfalseShows 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) => stringthe numberVisible 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.
disabledbooleanfalseDisables the native inputs. Inside a Field, this comes from the Field.
name · formstring—Form attributes for the native input(s). RangeSlider submits two values under one name.
aria-label · aria-labelledby · aria-describedbystring—Name and description when there is no visible label. For RangeSlider, each thumb adds its own name.
className · …div propsHTMLAttributes<HTMLDivElement>—Go to the root element; the forwarded ref points at it.

Accessibility

Keyboard

KeyBehaviour
TabMoves to each thumb in turn
→ / ↑Increases by keyStep (← in right-to-left pages)
← / ↓Decreases by keyStep (→ in right-to-left pages)
Page Up / Page DownChanges by pageStep
Home / EndMinimum 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

FigmaCode
Text row: Label, (Optional), valuelabel (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)

Do and don't

DoShow the value People see what they picked without counting ticks.
Don'tHide the value A bare track gives no idea of the number.
DoUse few, short mark labels Labels stay readable on a phone.
Don'tLabel every step Crowded labels overlap at small widths.