Components / forms
Slider
Radix is permitted here — Slider is on the closed list of six.
Summary
A value picked by position rather than typing.
| APG pattern | Slider, Multi-Thumb Slider |
| Built on | Radix |
| Status | stable |
| Since | 0.1.0 |
Pass a number for one thumb, an array for a range.
<Slider id="price" label="Price range" defaultValue={[40, 120]} min={0} max={200} step={10}
valueText="£40 – £120" thumbLabels={['Minimum price', 'Maximum price']} minStepsBetweenThumbs={1} />
Anatomy
A track with a filled range and one or two thumbs, plus a header carrying the label and the current value.
| Part | Element | Required |
|---|---|---|
header | <div> | no |
label | <span> | no |
range | <radixslider.range> | yes |
root | <radixslider.root> | yes |
thumb | <radixslider.thumb> | yes |
track | <radixslider.track> | yes |
value | <span> | yes |
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
value | number | number[] | none | A number for one thumb, an array for a range. |
defaultValue | number | number[] | 50 | A number for one thumb, a pair for a range. The shape of this decides which the slider is. |
onValueChange | (value: number | number[]) => void | none | Called as the thumb moves, with a number or a pair to match value. |
min | number | 0 | The bottom of the range. |
max | number | 100 | The top of the range. |
step | number | 1 | How far one arrow key moves the thumb. Anything a reader would type instead wants a Spinbutton. |
label | React.ReactNode | none | Drawn above the track and used as the group's accessible name. Each thumb still needs its own — see thumbLabels. |
valueText | string | string[] | none | Overrides the printed value AND is exposed as aria-valuetext, so a screen reader hears "40 pixels" rather than "40". Pass an array to give each thumb of a range its own text. |
thumbLabels | string[] | none | One accessible name per thumb. Required for a range: two controls both called "Price" are indistinguishable by keyboard or screen reader. |
minStepsBetweenThumbs | number | none | Steps that must remain between adjacent thumbs. Thumbs never cross — they push — so without this a range can be dragged down to zero width. |
disabled | boolean | none | Removes the thumbs from the tab order. |
id | string | none | Optional. A stable id is generated when omitted; the name never depends on it. |
States
| State | Expressed as | |
|---|---|---|
| Pressed | :active | |
| Focused | :focus-visible | |
| Hover | :hover | |
| Disabled | data-disabled |
Behavior
Arrow keys move by step, Home and End go to the ends. A range's thumbs cannot cross, and minStepsBetweenThumbs keeps a gap between them.
Accessibility
Each thumb needs its own name. Minimum price and Maximum price — two thumbs both called "Price range" are indistinguishable to anyone not looking at them.
valueText is what a reader hears. The raw number is rarely the unit they think in.
No vertical orientation. That is a capability gap rather than an oversight, and it is recorded in the upstream port rather than hidden in a test.
Keyboard
| Key | Result |
|---|---|
Arrow keys | Move the thumb by one step |
Home / End | Jump to the minimum or maximum |
Page Up / Page Down | Move by a larger increment |
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-labelledby | set at render |
aria-valuetext | set at render |
Focus
Each thumb is its own tab stop, which is why each needs its own name. Radix supplies the roving behavior; nothing here re-implements it.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It takes the touch floor under (pointer: coarse) — the visual size is unchanged on a desktop pointer.
Tested
test/apg/batch-a.test.jsxtest/apg/partials-sweep.test.jsx
Known gaps
- 3 upstream refs — APG's own page or markup, not the pattern
- 1 upstream ref — covered by our own dependent-range tests in partials-sweep
- 1 upstream ref — Slider has no vertical orientation. A capability gap, not a defect — nothing in the system asks for one yet.
- 4 upstream refs — sliders drawn as SVG rails
Appearance
| Token | Used for |
|---|---|
--se-accent | background, border |
--se-border-width | border |
--se-duration-instant | transition |
--se-ease-standard | transition |
--se-focus-color | outline |
--se-focus-offset | outline-offset |
--se-focus-width | outline |
--se-font-code | font-family |
--se-radius-pill | border-radius |
--se-shadow-floating | box-shadow |
--se-shadow-raised | box-shadow |
--se-space-2 | block-size, gap |
--se-space-3 | gap |
--se-surface | background |
--se-surface-sunk | background |
--se-target-min | min-block-size |
--se-text | color |
--se-text-disabled | background, border-color |
--se-text-muted | color |
--se-text-sm | font-size |
--se-thumb-size | block-size, inline-size |
--se-weight-medium | font-weight |
The thumb takes --se-shadow-raised at rest and --se-shadow-floating on hover. That is literal lift under a pointer and says nothing about dismissal — the one place a level means something other than what 3.5 reads.
Motion
The thumb's shadow crosses --se-duration-instant. The fill does not animate while dragging; it tracks the pointer.
Content
The label names the quantity. valueText carries the unit and the currency — the number alone is ambiguous the moment there is more than one slider on the page.
Usage
Use it when the precise figure barely matters. For an exact, small-range number the reader knows, a Spinbutton reads faster — and when both are true, show both and let them share state.
When not to use it. For a value with fewer than about five useful positions. That is a RadioGroup.
Notes
Radix is permitted here — Slider is on the closed list of six.