Slider

Picks a number (or a range between two thumbs) from a continuous scale by dragging or arrow keys — for values where "about this much" is the natural way to choose.

Basic usage

Install the package:

pnpm add @design-schema/react @design-schema/tokens

Import the stylesheet once for the app, then the component:

import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Slider } from '@design-schema/react';

Examples

Volume40
import { Slider } from '@design-schema/react';

<Slider label="Volume" name="volume" defaultValue={40} />

Props

17 rows
PropTypeDefaultDescriptionPlatforms
label requiredstring—

Visible label naming the quantity ("Volume", "Price range").

Accessibility: The label is a Text `<span>` with an id, not a native `<label for>` (which cannot name a `div role=slider`); a single thumb takes aria-labelledby pointing at it (on Lit the span is in the same shadow root, so the id reference holds). A range slider's thumbs are named with aria-label (accessibilityLabel on native) set to the resolved `copy.minimumLabel` / `copy.maximumLabel`, with no hidden spans.

web, lit, rn, swiftui
name requiredstring—

Field name for the Form. The form value types (web/rn FormContext, Lit DsFormField) have no number, so a single value registers as its decimal string (`String(value)`) and a range as two strings `[String(low), String(high)]`; native form submission gets one hidden input (web) or FormData entry (Lit) per string, all under this name. The registration id and the Form's focus-on-error target is the thumb, the low thumb for a range (its first tab stop); a consumer-supplied `id` lands there too, and the high thumb carries no id.

web, lit, rn, swiftui
minnumber0

Lower bound.

web, lit, rn, swiftui
maxnumber100

Upper bound.

web, lit, rn, swiftui
stepnumber1

Arrow-key increment and snapping granularity for drag, click and keys. The step grid is anchored at `min` (`min + round((raw - min) / step) * step`); when `(max - min)` is not a whole number of steps the last partial step still snaps to `max`, so the maximum is reachable by drag and click and not only by End.

web, lit, rn, swiftui
snapToMarksbooleanfalse

With `marks`, snap drag and click to the marks instead of `step` (arrow keys still move by step; PageUp/Down go to the next mark, and past the last mark to `max`/`min`). This is the only switch for mark snapping: omitting `step` changes nothing, since it defaults to 1. Set without any `marks` the flag is inert — drag and click fall back to the step grid and PageUp/Down to ten steps.

web, lit, rn, swiftui
requiredbooleanfalse

Must have a value other than the default to submit (`copy.required`). "The default" is `defaultValue` when set and otherwise what `value` itself falls back to — `min`, or `[min, max]` for a range — clamped to [min, max] before comparing, so required and value share one notion of it. For a range, a difference in either component of the pair satisfies it: moving one thumb is enough. The label takes no "(required)" suffix here: a slider always shows a value, so the suffix would say nothing about what is missing. Required is a validity flag (valueMissing, message `copy.required`) checked before invalid, but its message is not rendered standalone: the error region shows `error`, else the message a Form (or `validate`) has reported, else `copy.invalid` when `invalid`.

web, lit, rn, swiftui
invalidbooleanfalse

Marks the slider invalid (`copy.invalid` when no `error`). There is no invalid colour for the track: a slider has no text to recolour and no border of its own, so the state is carried by aria-invalid and the error message. `invalid` and `error` are independent: aria-invalid is true when either is set, and setting or clearing `error` never changes the `invalid` prop — so error identification is asserted on `aria-invalid="true"` on the thumb (the element carrying the role), never on the `invalid` prop.

web, lit, rn, swiftui
valuenumber | [number, number]—

Controlled value; for a range, a two-number array.

web, lit, rn, swiftui
defaultValuenumber | [number, number]—

Initial value (or pair). Defaults to `min` (or `[min, max]`).

web, lit, rn, swiftui
rangebooleanfalse

Two thumbs choosing a minimum and a maximum; the thumbs cannot cross. A mismatch between `range` and the shape of `value`/`defaultValue` (`range: true` with a number, or a pair without `range`) falls back silently to that mode's default — `[min, max]` for a range, `min` for one thumb — with no development warning; only `max <= min` warns.

web, lit, rn, swiftui
formatValuefunction (value: number) => string—

Renders the displayed and announced value ("$40", "3 h 20 min"). Defaults to the number. `aria-valuetext` is always emitted, including in the default case where it repeats `aria-valuenow`.

web, lit, rn, swiftui
showValuealways | hover | neveralways

Where the value text appears: always beside the label, only while dragging or focused (as a bubble above the thumb), or not at all (when a NumberInput beside the slider shows it). Despite its name, `hover` means pressed or focused: plain pointer hover does not show the bubble, and touch platforms behave identically. Any focus counts (not only :focus-visible), including focus a pointer press moves to the thumb; on React Native a core View reports focus only under react-native-web, so on a device the bubble is press-only. The bubble element exists only under `hover` — `always` shows the value beside the label and never a drag bubble. A range renders one bubble per thumb, each shown only while its own thumb is pressed or focused; every bubble is aria-hidden (active or not), since the thumb's aria-valuetext already announces the same value, and the inactive one stays rendered at opacity 0 so `transition` can fade it. The bubble sizes to its content on one line, centred on the thumb; it is not constrained by the thumb's hit area and may overflow it.

web, lit, rn, swiftui
marksarray { value: number; label?: string | undefined }[]—

Tick marks on the track, optionally labelled. Values snap to marks only with `snapToMarks`. Generated code exports the entry type as `SliderMark`. The dots are the `tickMarks` part (aria-hidden): a layer on the track centre line inside the track area. The labels sit in an unparted aria-hidden row below the track area (see `markLabelGap`); each label is a Text (size xs, tone muted, element span) centred under its mark by a 50% offset (mirrored in right-to-left). There is no collision handling and no clamping at the ends: labels on close marks may overlap, and the labels at `min` and `max` hang past the track. Presses on the label row do not move a thumb — the label row and the mark label row paint above the thumbs and take the press, which is also how the overlap with a thumb's `minTarget` hit area is resolved.

web, lit, rn, swiftui
disabledbooleanfalse

Not adjustable, still readable: thumbs stay focusable but pointer, keys and accessibility actions are ignored, and no value is submitted (on Lit `currentValue` is null while disabled). A slider disabled by an enclosing Form or Fieldset (Lit formDisabledCallback) behaves the same. On React Native the thumb View keeps `accessible` and sets accessibilityState.disabled.

web, lit, rn, swiftui
descriptionstring—

Helper text.

web, lit, rn, swiftui
errorstring—

Error message. `error` is not in `form.validation`, so it is reported as a custom validity (web `setCustomValidity`, Lit `setValidity({ customError: true }, error)`) and blocks submission like the listed flags; the validity message follows the precedence `error`, then `copy.required`, then `copy.invalid`.

web, lit, rn, swiftui

Events

2 rows
EventTypeDescriptionPlatforms
onChangeonChange / change / onValueChange

Fired on every value change while dragging or with keys (number or pair); only when the value actually changed, so a key at a bound or a click on the thumb fires nothing. Within one interaction the comparison is against the last value emitted, not the displayed value, so a controlled owner that never updates `value` gets each new target once; the displayed value resets from the prop on render.

web, lit, rn, swiftui
onChangeEndonChangeEnd / change-end / onSlidingComplete

Fired once when the interaction ends (pointer up, key released), and only if that interaction changed the value (End at max or a click on the thumb fires nothing). Use for expensive effects.

web, lit, rn, swiftui

Accessibility

Decisions this component leaves to you

Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.

Supply the label. It is associated with the control for you, but the words are yours — a placeholder is not a label.

If you wire up the gesture events, provide the pointer and keyboard path to the same outcome.

Supply the error text when the value is invalid. The component links and announces it; it cannot write it.

Required props that carry accessibility meaning: label. Each is described in the props table above.

Implicit ARIA role: slider. React Native renders role adjustable.

Anatomy

labeltrackfillthumbvalueTextbubbletickMarksdescriptionerrorMessage

What generation guarantees

Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.

Label association — The label is programmatically associated with the control, not merely placed beside it.

Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.

Arrow navigation — Arrow keys move between items; Home and End jump to the ends.

Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.

Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.

Target size — Interactive targets are at least 44×44 CSS pixels — the touch-first size, not the minimum.

Gesture alternative — Every gesture — swipe, drag, long-press — has a single-pointer and a keyboard equivalent.

Error identification — An invalid value is identified in text and linked to the control — never signalled by colour alone.

Reduced motion — Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.

Slider keyboard support

6 rows
KeyAction
ArrowRight or ArrowUpIncreases by `step`. ArrowRight is mirrored in a right-to-left layout (it decreases there), read from the thumb's — on Lit the host's — computed `direction` at keydown, as in Tabs and SegmentedControl; ArrowUp always increases.
ArrowLeft or ArrowDownDecreases by `step`. ArrowLeft is mirrored in a right-to-left layout (it increases there); ArrowDown always decreases.
PageUp or PageDownChanges by ten steps, clamped to the bounds (with `snapToMarks`, to the next mark, and to `max`/`min` past the last mark). Not mirrored in right-to-left.
HomeSets the minimum — for a range thumb the live constraint from the other thumb, not `min`, so the action can never cross the thumbs.
EndSets the maximum — for a range thumb the live constraint from the other thumb, not `max`.
TabMoves between the two thumbs of a range slider; each thumb is a tab stop. (range)

Contrast pairs

4 rows
ForegroundBackgroundLevel
color.control.selectedBackgroundcolor.backgroundAA (non-text)
color.foregroundcolor.backgroundAA
color.foreground.mutedcolor.backgroundAA
color.inverse.foregroundcolor.inverse.surfaceAA

Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.