Range
Range — a slider, single or dual-thumb.
Examples
<div class="field">
<label class="field-label" for="demo-volume">Volume</label>
<input class="range" id="demo-volume" type="range" min="0" max="100" value="60"
data-ui="range" />
</div><div class="field">
<span class="field-label" id="demo-price-label">Price</span>
<div class="range-multi" data-ui="range" data-range-gap="5" role="group"
aria-labelledby="demo-price-label">
<label class="sr-only" for="demo-price-lo">Minimum price</label>
<input class="range" id="demo-price-lo" type="range" min="0" max="100" value="20" />
<label class="sr-only" for="demo-price-hi">Maximum price</label>
<input class="range" id="demo-price-hi" type="range" min="0" max="100" value="70" />
</div>
</div>Each thumb has its own name. “Price” twice is indistinguishable to anyone listening rather than looking.
The two thumbs cannot meet: data-range-gap is the smallest range the control may express, and it defaults to one step even when you do not set it. Letting them land on the same value collapses the range to a point — one thumb where there are two, no painted fill, and the only clue that a second control exists is a thumb appearing out of nowhere when you drag. Do not reach for .range-sm here either: two thumbs share one track, so the spacing exception in WCAG 2.2 target size does not apply and each thumb has to clear 24px on its own, which the default does and the small size does not.
Tokens 15
Level 2, declared on .range itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --range-a | 0 |
| --range-b | 1 |
| --range-fill | 0 |
| --range-fill-color | var(--color-primary) |
| --range-fill-position | calc( var(--range-thumb-size) / 2 + (100% - var(--range-thumb-size)) * var(--range-fill) ) |
| --range-radius | var(--radius-full) |
| --range-thumb-border-color | var(--color-primary) |
| --range-thumb-border-width | var(--border-width-strong) |
| --range-thumb-color | var(--color-surface-raised) |
| --range-thumb-size | 1.5rem |
| --range-track-border-color | var(--color-border) |
| --range-track-color | var(--color-surface-sunken) |
| --range-track-size | 0.375rem |
| --range-a-position | calc( var(--range-thumb-size) / 2 + (100% - var(--range-thumb-size)) * var(--range-a) ) |
| --range-b-position | calc( var(--range-thumb-size) / 2 + (100% - var(--range-thumb-size)) * var(--range-b) ) |
Variants and states
Variants
.range-lg.range-sm
Inside it
.range-multi
State it reads
Read from the platform, never mirrored into a class that could disagree with it.
:disabled:focus
Before you ship it
This one needs a script. Opt the markup in with data-ui="range" and import import 'mostlycss/js/components/range' — that specifier and not the package root, which is a bundle carrying a second copy of the registry, so a page holding both initialises every element twice. What you lose without the script is listed below — for some components that is only polish, for others it is the whole interaction.
What you have to do 1 requirements
- A range input has no visible label of its own. Give every input an accessible name — and in the dual case give each of the two inputs its own name ("Minimum price", "Maximum price"), because "Price" twice is indistinguishable to anyone listening rather than looking. Wrap the pair in
role="group"with a name for the pair itself. A.range-multinever lets its two thumbs meet:range.tskeeps them at least one step apart, ordata-range-gapapart when you set it. Two thumbs on the same value collapse the range to a point and read as a single-thumb slider with a painting bug. In a.range-multithe first input is the lower bound and the second is the upper bound. The behaviour enforces that ordering; the markup must not contradict it.
src/css/components/range.css · npx mostlycss add range