Component
range
Demo
tokens.css,
base.css and range.css. No scripts.
Source
Copy both files, or run npx nojsui add range.
Browser support
Works across current and earlier versions of every major engine.
| Feature | Baseline | Chrome | Edge | Firefox | Safari | Chrome Android | Firefox Android | Safari iOS |
|---|
| Feature | Baseline | Chrome | Edge | Firefox | Safari | Chrome Android | Firefox Android | Safari iOS |
|---|---|---|---|---|---|---|---|---|
| accent-color | limited | 93 | 93 | 92 | 26.2 | — | 92 | 26.2 |
Usage
A real <input type="range">, coloured from the token palette, with optional
native tick marks.
<div class="sk-range">
<label class="sk-range__label" for="volume">Volume</label>
<input class="sk-range__input" id="volume" type="range" min="0" max="100" value="60" />
</div>
Add ticks with a <datalist>. The browser draws them; nothing in this
component’s CSS does:
<input class="sk-range__input" id="quality" type="range" min="0" max="3" list="quality-ticks" />
<datalist id="quality-ticks">
<option value="0" label="Draft"></option>
<option value="3" label="Best"></option>
</datalist>
There is no value bubble, and there cannot be one
The obvious feature for this component is a bubble showing the current value. It is not here, and the reason is worth stating precisely, because it is the kind of thing that looks like an oversight.
Dragging a range updates the value IDL property and never touches the
value content attribute. attr() reads the content attribute. Measured in
Chromium: after ten ArrowRight presses on <input type="range" value="50">,
input.value is "60" while getAttribute("value") — and attr(value) in
CSS — are both still 50. There is no CSS way to read a range’s live value.
So a live bubble needs a script to mirror the IDL property onto the attribute,
and this kit does not ship script. docs/polyfills.md has the handful of lines
if you want one in your own project.
A bubble showing only the default value was considered and rejected: it would display a number that stops matching the slider the instant anyone touches it, which is the same class of lie as a progress bar frozen at 100%. ADR 0017 has the full reasoning and records the deviation from the PRD.
appearance: none is also deliberately absent
Restyling a range’s track and thumb means ::-webkit-slider-thumb and
::-moz-range-thumb — vendor pseudo-elements, not standard, and two of them to
keep in sync indefinitely. A native range tinted with accent-color is a good,
familiar control in all three engines.
It also keeps the tick marks. They are drawn by the user agent, and
appearance: none removes them along with the rest of the native rendering.
How it works
accent-color tints the filled part of the track and the thumb. base.css
already sets it from the palette on :root; the --sk-range-accent knob is
what lets one range differ from the rest without touching a global token.
The tick marks come entirely from <datalist>. All three engines render them,
and they render them differently — verified 2026-08-21:
| Engine | Tick rendering |
|---|---|
| Chromium | Small marks below the track |
| Firefox | Small marks below the track |
| WebKit | Dots on the track itself |
That difference is the platform’s, not the component’s, and it is why the visual snapshots differ per engine.
The .sk-range__scale row places each name as a zero-width point that its text
overflows equally, so space-between lands the points at exactly 0%, 33%, 67%
and 100% of the row. Equal-width columns would put them at 12.5/37.5/62.5/87.5%
instead — visibly wrong against the ticks. One gap remains and cannot be
closed: the user agent insets the track by half a thumb at each end, and the
thumb’s width is not exposed to CSS, so the names sit a few pixels outside
their marks. The tick marks are the precise indicator; the names label them.
Keyboard contract
Entirely native.
| Key | Behaviour |
|---|---|
Tab | Moves focus to the slider |
← / ↓ | Decreases by one step |
→ / ↑ | Increases by one step |
Home / End | Jumps to min / max |
Page Up / Page Down | Larger jump, size chosen by the engine |
Verified manually in VoiceOver and NVDA: yes — announced as a slider with its label, its numeric value, and its range.
Accessibility notes
The accessible name is the <label for>. The .sk-range__scale row is
aria-hidden because it duplicates what the control already conveys.
A screen reader announces the number, not the step’s name. In the
“Export quality” demo the visible scale reads Draft / Low / Good / Best, but
the announced value is 2. Naming the current step requires aria-valuetext,
which has to change as the value changes — and that needs script, for the same
reason the bubble does.
If the names matter more than the ordering does, a <select> is the better
control: it announces the chosen option’s text, it is smaller on a phone, and
it does not ask the user to hit a target. Use a range when the value is
genuinely continuous or the ordering is the point.
The focus ring comes from base.css and is not overridden. Because the
component does not use appearance: none, the thumb also keeps whatever
platform focus affordance the engine provides.
Degradation
| Feature used | Baseline status | Behavior without it |
|---|---|---|
accent-color | Chrome 93+, Firefox 92+, Safari 26.2+ | The range renders in the platform’s own accent colour instead of the kit’s. A completely normal control; only the tint differs. |
<datalist> on a range | widely | No tick marks. The slider still has the same min, max and step, so behaviour is identical and only the visual guide is missing. |
Nothing here is a floor: with neither feature you still get a working, correctly labelled native slider.
Theming
| Property | Default | Controls |
|---|---|---|
--sk-range-accent | --sk-color-accent | Track fill and thumb colour |
--sk-range-label-fg | --sk-color-text | Label text |
--sk-range-scale-fg | --sk-color-text-subtle | The step-name row under the track |
--sk-range-gap | --sk-space-2xs | Space between label, slider and scale |