Skip to main content

Theme config

Theme mode

Color palette

Grays

Border radii/radiuses/radiopedes/you know
Border radius
Field border radius
Button border radius
All
Components
Guides
API
Recent

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

  1. Container
  2. Label (optional)
  3. Value (optional)
  4. Start text (optional)
  5. Input
  6. 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;
}
}
}
}
}