Range

input.range[type="range"][data-ui="range"]

Range — a slider, single or dual-thumb.

Examples

Single thumb

View “Single thumb” as
<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>

Dual thumb

View “Dual thumb” as
Price
<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.

Level 2 tokens declared by range
TokenDefault
--range-a0
--range-b1
--range-fill0
--range-fill-colorvar(--color-primary)
--range-fill-positioncalc( var(--range-thumb-size) / 2 + (100% - var(--range-thumb-size)) * var(--range-fill) )
--range-radiusvar(--radius-full)
--range-thumb-border-colorvar(--color-primary)
--range-thumb-border-widthvar(--border-width-strong)
--range-thumb-colorvar(--color-surface-raised)
--range-thumb-size1.5rem
--range-track-border-colorvar(--color-border)
--range-track-colorvar(--color-surface-sunken)
--range-track-size0.375rem
--range-a-positioncalc( var(--range-thumb-size) / 2 + (100% - var(--range-thumb-size)) * var(--range-a) )
--range-b-positioncalc( 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

  1. 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-multi never lets its two thumbs meet: range.ts keeps them at least one step apart, or data-range-gap apart 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-multi the 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