Components
Range
Full support Supported since v125. Full support Supported since v128. Full support Supported since v18.
<label class="range"> <span class="label">Label</span> <span class="start-text">Min</span> <input type="range" /></label>Start text & End text
<label class="range"> <span class="label">Label</span> <span class="start-text">Start helper text</span> <input type="range" /> <span class="end-text">End helper text</span></label>Value
Add an <output class="value"> sibling to the
.label with for pointing at the input's id. Optionally
set data-suffix for a unit (e.g. °,
px). Updating its text content is the consumer's
responsibility.
<label class="range"> <span class="label">Hue</span> <output class="value" for="hueRange" data-suffix="°">250°</output> <input type="range" id="hueRange" min="0" max="360" value="250" /></label>Tick marks
Use the list attribute on the <input> and
follow it with a <datalist> element containing
<option> elements with value and
label attributes.
<label class="range"> <span class="label">Tick marks with labels</span> <input list="labeled-markers" type="range" /> <datalist id="labeled-markers"> <option value="0" label="0%"></option> <option value="25" label="25%"></option> <option value="50" label="50%"></option> <option value="75" label="75%"></option> <option value="100" label="100%"></option> </datalist></label>Variants
Use the .filled, .default, or .tonal
class to swap the track surface for better contrast on different backgrounds.
<label class="range"> <input type="range" /> <span class="label">Default</span></label><label class="range default"> <input type="range" /> <span class="label"><code>default</code> = <code>var(--surface-default)</code></span></label><label class="range filled"> <input type="range" /> <span class="label"><code>filled</code> = <code>var(--surface-filled)</code></span></label><label class="range tonal"> <input type="range" /> <span class="label"><code>tonal</code> = <code>var(--surface-tonal)</code></span></label>Disabled
<label class="range"> <span class="label">Disabled</span> <input type="range" disabled /></label>Validation
<label class="range" data-invalid> <span class="label">Invalid Range</span> <input type="range" /> <span class="end-text">This value is incorrect.</span></label>Spread
<label class="range spread"> <span class="label">Spread Layout</span> <span class="start-text">Start text</span> <input type="range" /> <span class="end-text">End text</span></label>
<label class="range spread"> <span class="label">Disabled</span> <span class="start-text">Start text</span> <input type="range" disabled /> <span class="end-text">End text</span></label>
<label class="range spread" data-invalid> <span class="label">Invalid Range</span> <span class="start-text">Start text</span> <input type="range" /> <span class="end-text">This value is incorrect.</span></label>
<label class="range spread"> <span class="label">Tick marks with labels</span> <input list="labeled-markers-spread-html" type="range" /> <datalist id="labeled-markers-spread-html"> <option value="0" label="0%"></option> <option value="25" label="25%"></option> <option value="50" label="50%"></option> <option value="75" label="75%"></option> <option value="100" label="100%"></option> </datalist></label>Disabled
<div class="range spread" disabled> <label class="label">Volume</label> <input type="range" min="0" max="100" value="50" disabled /></div>Validation
<div class="range spread" data-invalid> <label class="label">Volume</label> <input type="range" min="0" max="100" value="50" /></div>Accessibility
- Right Arrow: Increase the value of the slider by one step.
- Up Arrow: Increase the value of the slider by one step.
- Left Arrow: Decrease the value of the slider by one step.
- Down Arrow: Decrease the value of the slider by one step.
- Home: Set the slider to the first allowed value in its range.
- End: Set the slider to the last allowed value in its range.
- Page Up (Optional): Increase the slider value by an amount larger than the step change made by Up Arrow.
- Page Down (Optional): Decrease the slider value by an amount larger than the step change made by Down Arrow.
Anatomy
- Container
- Label (optional)
- Value (optional)
- Start text (optional)
- Input
- End text (optional)
API
| Type | Modifiers | Default | Description |
|---|---|---|---|
| Input | input[type="range"] | - | The native range input element. |
| Range | .range | - | Wrapper for label and input styling. |
| Spread | .spread | - | Modifier for a spread layout. |
| Variant | .filled, .default, .tonal | - | Modifiers for different background surfaces. |
| Label | .label | - | The label element for the range. |
| Value | output.value | - | Optional <output> showing the input's current value. Use
for to associate it with the input and an optional
data-suffix attribute for a unit suffix. |
| Start text | .start-text | - | Optional text displayed between label and the range input (often used with spread layout). |
| End text | .end-text | - | Optional text displayed below the range input. |
| Spread | .spread | - | Modifier class to layout label and input on opposite sides. |
Browser support
Full support Supported since v125. Full support Supported since v128. Full support Supported since v18.
See also the full browser support guide.
Installation
@layer components.root { :where(.range) { display: grid; gap: var(--size-1) 0; /* Two-column base so the optional .value cell can sit next to the label on row 1. Children that span the full width keep using `grid-column: 1/-1`, so this is backwards compatible. */ grid-template-columns: 1fr auto; position: relative;
.label { color: var(--text-primary); font-size: var(--font-size-05); font-weight: 600; grid-column: 1; grid-row: 1; }
/* Live current-value readout. Pairs with the <output> element rendered by the Range component (or hand-written .value markup). */ :where(.value) { color: var(--text-muted); font-size: var(--font-size-0); font-variant-numeric: tabular-nums; font-weight: 600; grid-column: 2; grid-row: 1; line-height: 1.5; text-align: end; }
.start-text { color: var(--text-muted); font-size: var(--font-size-0); grid-column: 1/-1; grid-row: 2; line-height: 1.5; }
:where(.end-text) { color: var(--text-muted); font-size: var(--font-size-0); grid-column: 1/-1; grid-row: 5; line-height: 1.5; }
/* Datalist (tick mark labels) */ :where(datalist) { color: var(--text-muted); display: flex; font-size: var(--font-size-0); grid-column: 1/-1; grid-row: 4; justify-content: space-between; margin-top: calc(var(--size-1) * -1); padding-inline: calc(3ex / 2);
&>option { display: flex; inline-size: 0; justify-content: center; padding: 0; white-space: nowrap; } }
/* Disabled */ &:has([disabled]) { cursor: not-allowed; opacity: 0.64; user-select: none;
.label, .value, .start-text, .end-text { cursor: not-allowed; } }
/* Color */ &.filled input[type="range"] { --_track-color: var(--surface-filled); }
&.default input[type="range"] { --_track-color: var(--surface-default); }
&.tonal input[type="range"] { --_track-color: var(--surface-tonal); }
/* Orientation */ &.spread { align-items: center; column-gap: var(--size-4); container-type: inline-size; grid-template-columns: 1fr auto;
.label { grid-column: 1; grid-row: 1; }
.start-text { color: var(--text-muted); display: block; font-size: var(--font-size-0); grid-column: 1; grid-row: 2; line-height: 1.5; }
input[type="range"] { align-self: center; grid-column: 2; grid-row: 1; min-inline-size: 25ch; }
/* In spread mode, the value sits below the slider in col 2 (where end-text would normally land). When end-text is also present it shifts down a row to avoid collision. */ :where(.value) { grid-column: 2; grid-row: 2; }
:where(.end-text) { grid-column: 2; grid-row: 2; text-align: end; }
datalist { grid-column: 2; grid-row: 2; }
/* When both datalist and end-text exist, push end-text below the datalist so they don't collide on the same grid cell. */ &:has(datalist) :where(.end-text) { grid-row: 3; }
&:has(.value) :where(.end-text) { grid-row: 3; }
&:has(.value):has(datalist) :where(.end-text) { grid-row: 4; }
@container (width < 400px) { grid-template-columns: 1fr;
* { grid-column: 1/-1; }
.label { grid-row: 1; }
/* Share row 1 with the label: the label's text stays left-aligned in its full-width cell, while the value shrinks to fit and right-aligns. Visually reads as a "[name ........ value]" row. */ :where(.value) { grid-row: 1; justify-self: end; text-align: end; }
.start-text { grid-row: 2; }
input[type="range"] { grid-column: 1/-1; grid-row: 3; }
datalist { grid-column: 1/-1; grid-row: 4; }
:where(.end-text) { grid-row: 5; text-align: start; } } }
/* Validation */ &[data-invalid], &:has(:user-invalid) { --_thumb-bg: var(--color-9); --_thumb-highlight-color: oklch(from var(--color-9) 70% 100% h / 20%);
.start-text, .end-text, .value { color: var(--color-9); } }
/* Range input */ :where(& > input[type="range"]) { --_thumb-bg: var(--primary); --_thumb-highlight-color: oklch(from var(--primary) 70% 100% h / 20%); --_thumb-highlight-size: 0px; --_thumb-offset: -1.125ex; --_thumb-size: 3ex; --_track-color: var(--field-border-color); --_track-fill: 0%; --_track-fill-color: var(--primary); --_track-height: 0.75ex; --_track-radius: 1e5px;
appearance: none; background: transparent; block-size: var(--size-4); display: block; grid-column: 1/-1; grid-row: 3; inline-size: 100%; outline-offset: 1ex;
@media (hover: none) { --_thumb-offset: -11.5px; --_thumb-size: 30px; }
/* Track */ &::-webkit-slider-runnable-track { appearance: none; background: linear-gradient(to right, var(--_track-fill-color) var(--_track-fill, 0%), var(--_track-color) 0%); block-size: var(--_track-height); border-radius: var(--_track-radius); }
&::-moz-range-track { appearance: none; background: var(--_track-color); block-size: var(--_track-height); border-radius: var(--_track-radius); }
&::-moz-range-progress { appearance: none; background: var(--_track-fill-color); block-size: var(--_track-height); border-radius: var(--_track-radius); }
/* Ring */ &::-webkit-slider-thumb { appearance: none; background: var(--_thumb-bg); block-size: var(--_thumb-size); border: 3px solid var(--surface-default); border-radius: 50%; box-shadow: 0 0 0 var(--_thumb-highlight-size) var(--_thumb-highlight-color); cursor: ew-resize; inline-size: var(--_thumb-size); margin-block-start: var(--_thumb-offset);
@media (prefers-reduced-motion: no-preference) { transition: box-shadow 0.1s ease; }
.fieldset-item:focus-within & { border-color: var(--gray-14); } }
&::-moz-range-thumb { appearance: none; background: var(--_thumb-bg); block-size: var(--_thumb-size); border: 3px solid var(--surface-default); border-radius: 50%; box-shadow: 0 0 0 var(--_thumb-highlight-size) var(--_thumb-highlight-color); cursor: ew-resize; inline-size: var(--_thumb-size); margin-block-start: var(--_thumb-offset);
@media (prefers-reduced-motion: no-preference) { transition: box-shadow 0.1s ease; }
.fieldset-item:focus-within & { border-color: var(--gray-14); } }
/* Element state */ .range:has(:user-invalid) &, .range[data-invalid] & { --_track-fill-color: var(--color-9); }
&:not([disabled]) { &:hover { --_thumb-highlight-size: var(--size-1); }
&:active { --_thumb-highlight-size: var(--size-2); --_track-color: light-dark(oklch(from var(--field-border-color) calc(l * 0.9) c h), oklch(from var(--field-border-color) calc(l * 1.1) c h)); } }
&[disabled] { --_thumb-bg: oklch(from var(--text-primary) l c h / 50%); --_track-color: var(--field-border-color);
cursor: not-allowed;
&::-webkit-slider-thumb { cursor: not-allowed; }
&::-moz-range-thumb { cursor: not-allowed; } } } }
}