Pick one value from a continuous range, where the handle's position is the answer and the exact number is secondary. If the number is what matters — a price, a timeout, a quantity someone would rather type — that is a number Input, or a slider paired with one. The base is the native input[type=range], which is the only version that is correct with no runtime: dragging, arrow keys, and the value announcement all arrive free.
Drag it, or focus it and press the arrow keys — the movement, the stepping, and the value announcement are all the native element's. This one carries step="5", so it snaps; the rows below show the other two cases. Only the filled track is scripted, standing in for the layer-2 behaviour the contract describes: --ds-slider-fill cannot follow the thumb on its own. The number beside the label is host text.
The one axis, and it moves one thing: how thick the groove is. The handle stays the same pill at every rail — a knob that grew with its groove would make the axis mean two things, and the thick rail's whole point is that the groove becomes a container the handle sits inside. Thick also drops to radius.control, since a full radius at that height bends the rail into a lozenge. Every rail sets only --ds-slider-rail; the track height and the handle’s centring offset both derive from it, so the knob cannot come off-centre when a rail changes.
| Example | Rail | Modifier | Height | Usage |
|---|---|---|---|---|
| Thin | --thin | space.1 | Dense rows where the slider is one control among many | |
| Default | ds-slider | space.1-5 | Everywhere else — the same groove Progress uses | |
| Thick | --thick | size.control.sm | Matches the small button’s height so the two sit on one row; the fill drops to half presence so the groove does not outshout the handle |
Stepped or continuous is the native step attribute, never a modifier — the same call Input makes for type. The trap is the middle row: omitting step is not continuous, it is step 1, which merely looks continuous across a wide range.
| Example | Attribute | Behaves | Usage |
|---|---|---|---|
step="any" | Continuous | Truly analogue values — opacity, a volume mix, anything with no valid increment | |
step omitted | Steps by 1 | The default, and the one to be careful with — it reads as continuous but is not | |
step="5" | Snaps to fives | When only some values are meaningful — the canvas above uses this |
The handle is a solid action.primary.bg mark with no ring — dark on a light theme, light on a dark one. It shares the filled action.primary.hover and action.primary.active ladder with Button, so the system has one state direction for every primary fill. The focus ring sits on the thumb rather than the input box — the same placement exception text-entry controls take.
| Example | State | Driver | Handle | Usage |
|---|---|---|---|---|
| Rest | default | action.primary.bg | A solid mark — dark on light, light on dark, no ring | |
| Hover | :hover | action.primary.hover | The handle steps; the track does not move | |
| Active | :active | action.primary.active | Grabbed — held for the whole drag | |
| Focus | :focus-visible | focus.ring | Ring on the thumb, not the full-width box | |
| Disabled | :disabled | border.strong | Handle and filled track both step back — an inert control should not carry the loudest mark on the page |
Every key below is the native element's, not a script's — which is the whole argument for building on it.
| Key | Does |
|---|---|
←↓ | Decrease by one step |
→↑ | Increase by one step |
Home | Jump to min |
End | Jump to max |
Tab | Move to and from the slider as a single stop — the thumb is never a separate one |
A slider says "somewhere along this range" — the number is the host's to show.
| Verdict | Rule |
|---|---|
| Do | Use a slider only when the position is the answer; when the exact number matters, use a number Input or pair the slider with one. |
| Do | Name every slider through a FormField label or aria-label, and let the native element report its own value — never re-announce the number with a live region. |
| Do | Set a step that matches the decision; a continuous-feeling slider over whole-unit values invites a number the host will silently round. |
| Don’t | Set --ds-slider-fill without keeping it in sync with the value — a fill that disagrees with the handle is worse than the plain track the default ships. |
| Don’t | Rebuild a slider out of divs to get a range, ticks, or a value bubble. Those are layer-2 additions over this contract, not a replacement for the native element and everything it brings. |