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 supportSupported since v125.Full supportSupported since v128.Full supportSupported since v18.

    Anatomy

    • <Range>

      Container element.

    • v-slot:default

      The label for the range.

    • v-slot:value

      Shows the current value, with an optional valueSuffix.

    • v-slot:start-text

      Description text displayed above the input.

    • v-model

      The range input.

    • v-slot:end-text

      Supporting text displayed below the input.

    <script setup lang="ts">
    import { Range } from "opui-css/vue"
    </script>
    <template>
    <Range label="Label" startText="Min" />
    </template>
    <label class="ui-range"
    ><span class="ui-label" id="s0-1"><!--[-->Label<!--]--></span><!----><span
    class="ui-start-text"
    id="s0-2"
    ><!--[-->Min<!--]--></span
    ><input
    aria-describedby="s0-2"
    aria-labelledby="s0-1"
    type="range"
    /><!----><!----></label
    >

    Start text & End text

    <script setup lang="ts">
    import { Range } from "opui-css/vue"
    </script>
    <template>
    <Range
    label="Label"
    startText="Start helper text"
    endText="End helper text"
    />
    </template>
    <label class="ui-range"
    ><span class="ui-label" id="s1-1"><!--[-->Label<!--]--></span><!----><span
    class="ui-start-text"
    id="s1-2"
    ><!--[-->Start helper text<!--]--></span
    ><input
    aria-describedby="s1-2 s1-3"
    aria-labelledby="s1-1"
    type="range"
    /><!----><span class="ui-end-text" id="s1-3"
    ><!--[-->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.

    <script setup lang="ts">
    import { Range } from "opui-css/vue"
    </script>
    <template>
    <Range label="Hue" min="0" max="360" value="250" valueSuffix="°" />
    </template>
    <label class="ui-range"
    ><span class="ui-label" id="s2-1"><!--[-->Hue<!--]--></span
    ><output class="ui-value" for="s2-0" data-suffix="°"
    ><!--[-->250°<!--]--></output
    ><!----><input
    aria-labelledby="s2-1"
    id="s2-0"
    type="range"
    min="0"
    max="360"
    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>.

    <script setup lang="ts">
    import { Range } from "opui-css/vue"
    </script>
    <template>
    <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%' },
    ]"
    />
    </template>
    <label class="ui-range"
    ><span class="ui-label" id="s3-1"><!--[-->Tick marks with labels<!--]--></span
    ><!----><!----><input
    aria-labelledby="s3-1"
    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.

    <script setup lang="ts">
    import { Range } from "opui-css/vue"
    </script>
    <template>
    <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>
    </template>
    <!--[--><label class="ui-range"
    ><span class="ui-label" id="s4-1"><!--[-->Default<!--]--></span
    ><!----><!----><input
    aria-labelledby="s4-1"
    type="range"
    /><!----><!----></label
    ><label class="ui-range ui-default"
    ><span class="ui-label" id="s4-5"
    ><!--[--><code>default</code> =
    <code>var(--surface-default)</code><!--]--></span
    ><!----><!----><input
    aria-labelledby="s4-5"
    type="range"
    /><!----><!----></label
    ><label class="ui-range ui-filled"
    ><span class="ui-label" id="s4-9"
    ><!--[--><code>filled</code> =
    <code>var(--surface-filled)</code><!--]--></span
    ><!----><!----><input
    aria-labelledby="s4-9"
    type="range"
    /><!----><!----></label
    ><label class="ui-range ui-tonal"
    ><span class="ui-label" id="s4-13"
    ><!--[--><code>tonal</code> =
    <code>var(--surface-tonal)</code><!--]--></span
    ><!----><!----><input
    aria-labelledby="s4-13"
    type="range"
    /><!----><!----></label
    ><!--]-->

    Disabled

    <script setup lang="ts">
    import { Range } from "opui-css/vue"
    </script>
    <template>
    <Range disabled label="Disabled" />
    </template>
    <label class="ui-range"
    ><span class="ui-label" id="s5-1"><!--[-->Disabled<!--]--></span
    ><!----><!----><input
    aria-labelledby="s5-1"
    type="range"
    disabled
    /><!----><!----></label
    >

    Validation

    <script setup lang="ts">
    import { Range } from "opui-css/vue"
    </script>
    <template>
    <Range label="Invalid Range" error endText="This value is incorrect." />
    </template>
    <label class="ui-range" data-invalid
    ><span class="ui-label" id="s6-1"><!--[-->Invalid Range<!--]--></span
    ><!----><!----><input
    aria-describedby="s6-3"
    aria-labelledby="s6-1"
    type="range"
    /><!----><span class="ui-end-text" id="s6-3"
    ><!--[-->This value is incorrect.<!--]--></span
    ></label
    >

    Spread

    <script setup lang="ts">
    import { Range } from "opui-css/vue"
    </script>
    <template>
    <Range spread>
    Spread Layout
    <template #start-text>Start text</template>
    <template #end-text>End text</template>
    </Range>
    <Range spread disabled>
    Disabled
    <template #start-text>Start text</template>
    <template #end-text>End text</template>
    </Range>
    <Range spread error endText="This value is incorrect.">
    Invalid Range
    <template #start-text>Start text</template>
    </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%' },
    ]"
    />
    </template>
    <!--[--><label class="ui-range ui-spread"
    ><span class="ui-label" id="s7-1"
    ><!--[-->
    Spread Layout
    <!--]--></span
    ><!----><span class="ui-start-text" id="s7-2"><!--[-->Start text<!--]--></span
    ><input
    aria-describedby="s7-2 s7-3"
    aria-labelledby="s7-1"
    type="range"
    /><!----><span class="ui-end-text" id="s7-3"
    ><!--[-->End text<!--]--></span
    ></label
    ><label class="ui-range ui-spread"
    ><span class="ui-label" id="s7-5"
    ><!--[-->
    Disabled
    <!--]--></span
    ><!----><span class="ui-start-text" id="s7-6"><!--[-->Start text<!--]--></span
    ><input
    aria-describedby="s7-6 s7-7"
    aria-labelledby="s7-5"
    type="range"
    disabled
    /><!----><span class="ui-end-text" id="s7-7"
    ><!--[-->End text<!--]--></span
    ></label
    ><label class="ui-range ui-spread" data-invalid
    ><span class="ui-label" id="s7-9"
    ><!--[-->
    Invalid Range
    <!--]--></span
    ><!----><span class="ui-start-text" id="s7-10"
    ><!--[-->Start text<!--]--></span
    ><input
    aria-describedby="s7-10 s7-11"
    aria-labelledby="s7-9"
    type="range"
    /><!----><span class="ui-end-text" id="s7-11"
    ><!--[-->This value is incorrect.<!--]--></span
    ></label
    ><label class="ui-range ui-spread"
    ><span class="ui-label" id="s7-13"
    ><!--[-->Tick marks with labels<!--]--></span
    ><!----><!----><input
    aria-labelledby="s7-13"
    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

    <script setup lang="ts">
    import { Range } from "opui-css/vue"
    </script>
    <template>
    <Range label="Volume" :min="0" :max="100" :value="50" spread disabled />
    </template>
    <label class="ui-range ui-spread"
    ><span class="ui-label" id="s8-1"><!--[-->Volume<!--]--></span
    ><!----><!----><input
    aria-labelledby="s8-1"
    type="range"
    min="0"
    max="100"
    disabled
    value="50"
    /><!----><!----></label
    >

    Validation

    <script setup lang="ts">
    import { Range } from "opui-css/vue"
    </script>
    <template>
    <Range label="Volume" :min="0" :max="100" :value="50" spread error />
    </template>
    <label class="ui-range ui-spread" data-invalid
    ><span class="ui-label" id="s9-1"><!--[-->Volume<!--]--></span
    ><!----><!----><input
    aria-labelledby="s9-1"
    type="range"
    min="0"
    max="100"
    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

    PropTypeDefaultDescription
    endTextstring-Supporting text displayed below the input.
    errorbooleanfalseShows error styles.
    idstring-The id of the <input>. Generated when omitted and the value is shown.
    labelstring-The label for the range.
    liststring-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>.
    spreadbooleanfalsePushes the label and description to one side and the input to the other.
    startTextstring-Description text displayed above the input.
    v-modelnumber, string-The current value.
    valuenumber, string-The current value.
    valueSuffixstring-Shows the current value, with an optional valueSuffix.
    variant"default", "tonal", "filled"-The variant to use.

    Slots

    SlotDescription
    datalistExtra <option> elements for the <datalist>.
    defaultThe label for the range.
    end-textSupporting text displayed below the input.
    start-textDescription text displayed above the input.
    valueShows the current value, with an optional valueSuffix.

    Attributes that aren't props, such as max, min or step, go to the <input>.

    Browser support

    Full supportSupported since v125.Full supportSupported since v128.Full supportSupported since v18.

    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;
    }
    }
    }
    }
    }