Slider
A value chosen along a line: brightness, a rating, a price band. One thumb for a value, two for a range, and a field beside to type the number without the mouse.
Try it
Drag the thumb, or focus it and use the arrows; Home and End go to the ends. Type 101 in the field to see the error the drawing has.
The drawn complete version: label, unit, info, caption, the values at the ends, the balloon that is always shown and the field. Everything else on this page is the same component with parts left out. Underneath is the platform’s input type="range", so the keyboard, aria-valuenow, the submitted value and what a reader hears are the browser’s; the component paints the line, the fill, the ticks and the balloon itself, so every browser draws the same track.
The drawn usages
A range
There is no native input with two thumbs, so a range is two inputs on one line, each a real control named “Price range Minimum” and “Maximum”, with the fill between them. Only the top input takes the pointer, and the one whose thumb is nearer the pointer is raised as it moves over the track: either thumb drags, and a click on the line moves the nearer one, which is what a single input does. The two never cross; the low field’s ceiling is the high value and the high field’s floor the low value. When the values meet, the balloons overlap; recorded, not solved.
Where it departs from the drawing, and why
The drawn track is a light grey that reads at about 1.5:1 on white, the same problem the Switch had: a control’s identifying parts need 3:1 (WCAG 1.4.11). The empty line is border/strong, the token that exists for that, and the fill and the thumb’s edge are interactive/accent, measured against both surfaces and against the thumb’s own fill. The fill starts at the thumb’s centre and not at the track’s edge: the thumb travels the width less its own size, so a fill in percent of the whole width would miss it by up to half a thumb at the ends. The tag-like balloon the drawing offers as an alternative is not built.
Best practice, and the alternatives
A slider is for a value the reader feels more than knows: volume, brightness, a price band. For a value they know, a field alone is faster and more exact, and showInput gives both. The balloon always shown is the drawing’s choice; the common practice is to show it on hover and focus, and the same component does that with showValue off and the value in end or in the field. Ticks serve ten steps or fewer; more crowd the line. A range with two fields is the fullest form; one with balloons only is what the drawing calls “Range slider + Label”. For a choice among a few named options, a SegmentedControl says the options; a slider says the distance between them.
Accessibility
The input is named by the visible label through aria-labelledby, and a range adds Minimum and Maximum off screen. aria-valuetext is formatValue of the value, so a reader hears “75%” or “Very bright”, the words the balloon shows; the balloon itself is hidden from readers, since the input already says it. The caption and the field’s error describe the input. The info icon is a button whose Tooltip is its name. Focus draws a ring around the thumb.
Measures
| Part | Value |
|---|---|
| Thumb | 16, an edge of 2 in interactive/accent on surface/raised, elevation/sm |
| Line | 4 at radius full; border/strong empty, interactive/accent filled |
| Disabled | interactive/disabled empty, interactive/on-disabled filled and on the thumb’s edge: the filled part stays the more |
| Track box | 24 tall, so the ring fits; the line is inset by half a thumb |
| Balloon | caption/md Semibold on 20, 8 inline, radius sm, a caret of 4 |
| Field | the Input, sm, 64 wide (--slider-field-width) |
| Ticks | 2 × 4 in surface/raised, one per step |
| Ends | caption/md in text/secondary; an icon of 16 |
Using it
import { Slider } from 'alpenglow';
const [share, setShare] = useState(75);
<Slider
label="Percentage"
unit="%"
info="How much of the total this takes."
caption="Applied to every new booking."
start="0%"
end="100%"
showValue
showInput
formatValue={(v) => `${v}%`}
value={share}
onChange={setShare}
/>
// A range: two thumbs, the fill between them, a field at each end.
const [band, setBand] = useState<[number, number]>([3500, 8500]);
<Slider label="Price range" range min={1000} max={10000} step={100} showValue showInput
formatValue={(v) => `$${v.toLocaleString()}`} value={band} onChange={setBand} />| Prop | Type | Default |
|---|---|---|
label | string | required |
hideLabel | boolean | false |
unit | string | — |
info | string | — |
caption | React | — |
min / max / step | number | 0 / 100 / 1 |
range | true | — |
value / defaultValue | number, or [number, number] with range | min; [min, max] |
onChange | (value: number) => void, or a pair | — |
showValue | boolean | false |
formatValue | (value: number) => string | String |
ticks | boolean | false |
start / end | React | — |
showInput | boolean | false |
inputLabel | string | "label value" |
thumbLabels | [string, string] | ['Minimum', 'Maximum'] |
disabled | boolean | false |
name | string | — |
className | string | — |