Components
Range
Anatomy
<Range>Container element.
slot="default"The label for the range.
slot="value"Shows the current value, with an optional
valueSuffix.slot="start-text"Description text displayed above the input.
<input>The range input.
slot="end-text"Supporting text displayed below the input.
---import { Range } from "@opui/astro"---
<Range label="Label" startText="Min" /><label class="ui-range" ><span class="ui-label" id="range-label-7">Label</span ><span class="ui-start-text" id="range-start-2">Min</span ><input aria-describedby="range-start-2" aria-labelledby="range-label-7" type="range"/></label>Start text & End text
---import { Range } from "@opui/astro"---
<Range label="Label" startText="Start helper text" endText="End helper text" /><label class="ui-range" ><span class="ui-label" id="range-label-8">Label</span ><span class="ui-start-text" id="range-start-3">Start helper text</span ><input aria-describedby="range-start-3 range-end-2" aria-labelledby="range-label-8" type="range" /><span class="ui-end-text" id="range-end-2">End helper text</span></label>Value
Pass the valueSuffix prop (or use the value named slot) to render a live readout of the slider's current value next to the label. The component wires up an <output> element and keeps its text in sync with the input.
---import { Range } from "@opui/astro"---
<Range label="Hue" min="0" max="360" value="250" valueSuffix="°" /><label class="ui-range" ><span class="ui-label" id="range-label-9">Hue</span ><output class="ui-value" for="range-7" data-suffix="°">250°</output ><input aria-labelledby="range-label-9" id="range-7" max="360" min="0" type="range" value="250"/></label>Tick marks
Pass an id to the list prop together with an optionsarray - options=[{ value, label }] - and the component renders a matching <datalist>.
---import { Range } from "@opui/astro"---
<Range label="Tick marks with labels" list="labeled-markers" options={[ { value: 0, label: "0%" }, { value: 25, label: "25%" }, { value: 50, label: "50%" }, { value: 75, label: "75%" }, { value: 100, label: "100%" }, ]}/><label class="ui-range" ><span class="ui-label" id="range-label-10">Tick marks with labels</span ><input aria-labelledby="range-label-10" 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 variant prop to swap the track surface for better contrast on different backgrounds.
---import { Range } from "@opui/astro"---
<Range label="Default" /><Range variant="default"> <code>default</code> = <code>var(--surface-default)</code></Range><Range variant="filled"> <code>filled</code> = <code>var(--surface-filled)</code></Range><Range variant="tonal"> <code>tonal</code> = <code>var(--surface-tonal)</code></Range><label class="ui-range" ><span class="ui-label" id="range-label-11">Default</span ><input aria-labelledby="range-label-11" type="range" /></label><label class="ui-range ui-default" ><span class="ui-label" id="range-label-12" ><code>default</code> = <code>var(--surface-default)</code></span ><input aria-labelledby="range-label-12" type="range" /></label><label class="ui-range ui-filled" ><span class="ui-label" id="range-label-13" ><code>filled</code> = <code>var(--surface-filled)</code></span ><input aria-labelledby="range-label-13" type="range" /></label><label class="ui-range ui-tonal" ><span class="ui-label" id="range-label-14" ><code>tonal</code> = <code>var(--surface-tonal)</code></span ><input aria-labelledby="range-label-14" type="range"/></label>Disabled
---import { Range } from "@opui/astro"---
<Range disabled label="Disabled" /><label class="ui-range" ><span class="ui-label" id="range-label-15">Disabled</span ><input aria-labelledby="range-label-15" disabled type="range"/></label><script type="module"> for (let e of document.querySelectorAll(`.ui-range`)) { let t = e.querySelector(`input[type="range"]`), n = e.querySelector(`:scope > output.ui-value`); if (!t || !n) continue; let r = n.dataset.suffix ?? ``, i = () => { n.textContent = `${t.value}${r}`; }; (t.addEventListener(`input`, i), i()); }</script>Validation
---import { Range } from "@opui/astro"---
<Range label="Invalid Range" error endText="This value is incorrect." /><label class="ui-range" data-invalid ><span class="ui-label" id="range-label-16">Invalid Range</span ><input aria-describedby="range-end-3" aria-labelledby="range-label-16" type="range" /><span class="ui-end-text" id="range-end-3" >This value is incorrect.</span ></label>Spread
---import { Range } from "@opui/astro"---
<Range spread> Spread Layout <Fragment slot="start-text">Start text</Fragment> <Fragment slot="end-text">End text</Fragment></Range>
<Range spread disabled> Disabled <Fragment slot="start-text">Start text</Fragment> <Fragment slot="end-text">End text</Fragment></Range>
<Range spread error endText="This value is incorrect."> Invalid Range <Fragment slot="start-text">Start text</Fragment></Range>
<Range label="Tick marks with labels" list="labeled-markers-spread" spread options={[ { value: 0, label: "0%" }, { value: 25, label: "25%" }, { value: 50, label: "50%" }, { value: 75, label: "75%" }, { value: 100, label: "100%" }, ]}/><label class="ui-range ui-spread" ><span class="ui-label" id="range-label-17">Spread Layout</span ><span class="ui-start-text" id="range-start-4">Start text</span ><input aria-describedby="range-start-4 range-end-4" aria-labelledby="range-label-17" type="range" /><span class="ui-end-text" id="range-end-4">End text</span></label><label class="ui-range ui-spread" ><span class="ui-label" id="range-label-18">Disabled</span ><span class="ui-start-text" id="range-start-5">Start text</span ><input aria-describedby="range-start-5 range-end-5" aria-labelledby="range-label-18" disabled type="range" /><span class="ui-end-text" id="range-end-5">End text</span></label><label class="ui-range ui-spread" data-invalid ><span class="ui-label" id="range-label-19">Invalid Range</span ><span class="ui-start-text" id="range-start-6">Start text</span ><input aria-describedby="range-start-6 range-end-6" aria-labelledby="range-label-19" type="range" /><span class="ui-end-text" id="range-end-6" >This value is incorrect.</span ></label><label class="ui-range ui-spread" ><span class="ui-label" id="range-label-20">Tick marks with labels</span ><input aria-labelledby="range-label-20" list="labeled-markers-spread" type="range" /><datalist id="labeled-markers-spread"> <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
---import { Range } from "@opui/astro"---
<Range label="Volume" min={0} max={100} value={50} spread disabled /><label class="ui-range ui-spread" ><span class="ui-label" id="range-label-21">Volume</span ><input aria-labelledby="range-label-21" disabled max="100" min="0" type="range" value="50"/></label>Validation
---import { Range } from "@opui/astro"---
<Range label="Volume" min={0} max={100} value={50} spread error /><label class="ui-range ui-spread" data-invalid ><span class="ui-label" id="range-label-22">Volume</span ><input aria-labelledby="range-label-22" max="100" min="0" type="range" value="50"/></label>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.
API
Range API
| Prop | Type | Default | Description |
|---|---|---|---|
endText | string | - | Supporting text displayed below the input. |
error | boolean | false | Shows error styles. |
id | string | - | The id of the <input>. Generated when omitted and the value is shown. |
label | string | - | The label for the range. |
list | string | - | The id of the <datalist>. Needed with options. |
options | (string, number, { value: string, number; label?: string, undefined; })[] | - | Tick marks, rendered as <option> elements in a <datalist>. |
spread | boolean | false | Pushes the label and description to one side and the input to the other. |
startText | string | - | Description text displayed above the input. |
value | number, string | - | The current value. |
valueSuffix | string | - | Shows the current value, with an optional valueSuffix. |
variant | "default", "tonal", "filled" | - | The variant to use. |
Slots
| Slot | Description |
|---|---|
datalist | Extra <option> elements for the <datalist>. |
default | The label for the range. |
end-text | Supporting text displayed below the input. |
start-text | Description text displayed above the input. |
value | Shows the current value, with an optional valueSuffix. |
Input attributes, such as disabled, max, min, name and step, go to the <input>.
Browser support
See also the full browser support guide.
Installation
@layer components.root { :where(.ui-range) { --_motion: var(--motion, 1); display: grid; gap: var(--size-1) 0; grid-template-columns: 1fr auto; position: relative;
.ui-label { color: var(--text-primary); font-size: var(--font-size-05); font-weight: 600; grid-column: 1; grid-row: 1; }
:where(.ui-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; }
.ui-start-text { color: var(--text-muted); font-size: var(--font-size-0); grid-column: 1/-1; grid-row: 2; line-height: 1.5; }
:where(.ui-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;
.ui-label, .ui-value, .ui-start-text, .ui-end-text { cursor: not-allowed; } }
/* Color */ &.ui-filled input[type="range"] { --_track-color: var(--surface-filled); }
&.ui-default input[type="range"] { --_track-color: var(--surface-default); }
&.ui-tonal input[type="range"] { --_track-color: var(--surface-tonal); }
/* Orientation */ &.ui-spread { align-items: center; column-gap: var(--size-4); container-type: inline-size; grid-template-columns: 1fr auto;
.ui-label { grid-column: 1; grid-row: 1; }
.ui-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; }
:where(.ui-value) { grid-column: 2; grid-row: 2; }
:where(.ui-end-text) { grid-column: 2; grid-row: 2; text-align: end; }
datalist { grid-column: 2; grid-row: 2; }
&:has(datalist) :where(.ui-end-text) { grid-row: 3; }
&:has(.ui-value) :where(.ui-end-text) { grid-row: 3; }
&:has(.ui-value):has(datalist) :where(.ui-end-text) { grid-row: 4; }
@container (width < 400px) { grid-template-columns: 1fr;
* { grid-column: 1/-1; }
.ui-label { grid-row: 1; }
:where(.ui-value) { grid-row: 1; justify-self: end; text-align: end; }
.ui-start-text { grid-row: 2; }
input[type="range"] { grid-column: 1/-1; grid-row: 3; }
datalist { grid-column: 1/-1; grid-row: 4; }
:where(.ui-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%);
.ui-start-text, .ui-end-text, .ui-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);
transition: box-shadow calc(0.1s * var(--_motion)) ease;
.ui-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);
transition: box-shadow calc(0.1s * var(--_motion)) ease;
.ui-fieldset-item:focus-within & { border-color: var(--gray-14); } }
/* Element state */ .ui-range:has(:user-invalid) &, .ui-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; } } } }}