Slider
A styled, accessible range control built on a native <input type="range">. Because the
underlying element is real HTML, the browser supplies role="slider", the full keyboard contract
(arrow keys, Home/End, PageUp/PageDown), and screen-reader value announcements for free — no
JavaScript bridge needed. Colour and the filled-track portion are driven by CSS custom properties;
v-model carries a number.
The examples are organised by layer: the class reference is the standalone
@oriui/css layer, and the Framework API is the @oriui/vue component. Every
example is live — flip its code between HTML (the standalone classes, also your htmx / Astro / Svelte /
plain-HTML usage), Vue; HTML is the default.
Classes
A slider is a block wrapper plus BEM part classes. The ori-color_* utility repoints the one token
the component reads everywhere (--ori-color). The Vue props in Framework API
map 1:1 to these.
| Class | Type | Description |
|---|---|---|
ori-slider | Block | Required base class (wrapper div). Full-width by default. |
ori-color_* | Color | primary · secondary · success · warn · danger · info · surface |
ori-slider__label | Part | Flex row wrapping the visible label text and optional current value. |
ori-slider__value | Part | Current value badge inside the label row; tabular-nums, slightly dimmed. |
ori-slider__input | Part | The native ; receives the inline --ori-slider-pct custom property. |
data-disabled | State | Set on the wrapper when disabled is true; dims the control and changes the cursor to not-allowed. |
Live demo
Drag either handle to see v-model update in real time. The two sliders share no state — each
drives its own ref.
<!--
The filled portion is an inline CSS custom property — set --ori-slider-pct
to ((value - min) / (max - min)) * 100 from your own script.
-->
<div class="ori-slider ori-color_primary" style="--ori-slider-pct: 60%">
<label class="ori-slider__label" for="vol">
<span>Volume</span>
<span class="ori-slider__value">60</span>
</label>
<input id="vol" type="range" class="ori-slider__input" min="0" max="100" step="1" value="60" />
</div>
Colors
Every semantic role. The filled track, the thumb, and the focus ring all read --ori-color.
<!-- swap ori-color_primary for any semantic role -->
<div class="ori-slider ori-color_danger" style="--ori-slider-pct: 60%">
<label class="ori-slider__label" for="s-danger"><span>danger</span></label>
<input id="s-danger" type="range" class="ori-slider__input" min="0" max="100" step="1" value="60" />
</div>
Range (min / max / step)
min, max, and step map directly to the native <input> attributes. The component recomputes
--ori-slider-pct from these, so the filled portion stays accurate.
<div class="ori-slider ori-color_primary" style="--ori-slider-pct: 40%">
<label class="ori-slider__label" for="rating">
<span>Rating</span>
<span class="ori-slider__value">4</span>
</label>
<input id="rating" type="range" class="ori-slider__input" min="0" max="10" step="1" value="4" />
</div>
Show value
show-value renders the current number on the trailing end of the label row in a tabular-nums span.
Useful when the exact value matters to the user.
<!-- with value -->
<div class="ori-slider ori-color_primary" style="--ori-slider-pct: 72%">
<label class="ori-slider__label" for="sv1">
<span>Volume</span>
<span class="ori-slider__value">72</span>
</label>
<input id="sv1" type="range" class="ori-slider__input" min="0" max="100" step="1" value="72" />
</div>
<!-- without value — omit the span -->
<div class="ori-slider ori-color_primary" style="--ori-slider-pct: 72%">
<label class="ori-slider__label" for="sv2"><span>Volume</span></label>
<input id="sv2" type="range" class="ori-slider__input" min="0" max="100" step="1" value="72" />
</div>
Disabled
disabled sets the native disabled attribute on the <input> and adds data-disabled on the
wrapper, which applies 50 % opacity and a not-allowed cursor.
<div class="ori-slider ori-color_primary" data-disabled style="--ori-slider-pct: 30%">
<label class="ori-slider__label" for="dis"><span>Locked</span></label>
<input id="dis" type="range" class="ori-slider__input" min="0" max="100" step="1" value="30" disabled />
</div>
Commit on release
update:modelValue fires on every tick of a drag — ideal for a live preview, but it would flood an
undo history (or re-run an expensive effect) on every intermediate value. change fires once, when
the user releases the thumb or commits a keyboard step, carrying the settled number. Bind both:
v-model for the live value, @change for the commit.
<script setup>
import { ref } from 'vue';
const opacity = ref(100);
const undoStack = [];
function commit(value) {
// record ONE undo step for the whole drag — not one per tick
undoStack.push({ opacity: value });
}
</script>
<template>
<!-- v-model tracks the live drag (thumb + fill); @change commits the settled value -->
<OriSlider v-model="opacity" label="Opacity" :show-value="true" :min="0" :max="100" @change="commit" />
</template>
A drag from 100 → 40 streams update:modelValue the whole way, so the fill animates live — but
change emits a single 40 on release, so history gets one entry, not sixty. Keyboard adjustments
commit per step, though: each arrow-key press is its own change (one undo entry per press), since the
browser settles the value on every keystroke.
Common patterns
A settings panel with labelled sliders — the most common real-world composition.
<div style="display: flex; flex-direction: column; gap: 1.25rem; max-width: 24rem">
<div class="ori-slider ori-color_primary" style="--ori-slider-pct: 75%">
<label class="ori-slider__label" for="p-vol">
<span>Volume</span><span class="ori-slider__value">75</span>
</label>
<input id="p-vol" type="range" class="ori-slider__input" min="0" max="100" step="1" value="75" />
</div>
<div class="ori-slider ori-color_secondary" style="--ori-slider-pct: 75%">
<label class="ori-slider__label" for="p-bal">
<span>Balance</span><span class="ori-slider__value">50</span>
</label>
<input id="p-bal" type="range" class="ori-slider__input" min="-100" max="100" step="5" value="50" />
</div>
</div>
Accessibility
The accessibility contract holds across every layer — the standalone classes and the Vue component render the same attributes and keyboard behaviour.
- Renders a real
<input type="range">, so the browser suppliesrole="slider", the accessible value (aria-valuenow,aria-valuemin,aria-valuemax), and value announcements to screen readers with no extra ARIA needed. - The
labelprop renders a<label>linked to the input viafor/id(auto-generated viauseId()). Without alabel, pass anaria-labeloraria-labelledbyattribute — OriSlider setsinheritAttrs: falseand binds$attrsto the<input>, so the name lands on the real control.showValueis a visual supplement, not a name source — pair it withlabeloraria-label. disabledis the real native attribute; the wrapper receivesdata-disabledfor CSS only.- The focus ring appears on the thumb (
:focus-visible) and uses--ori-color, so it always matches the active colour role. - For the HTML layer, set
--ori-slider-pctinline to((value - min) / (max - min)) * 100%so the WebKit gradient fill reflects the current value; Firefox reads::-moz-range-progressdirectly from the element value and needs no custom property.
| Key | Action |
|---|---|
ArrowRight | Increases value by one step. |
ArrowUp | Increases value by one step. |
ArrowLeft | Decreases value by one step. |
ArrowDown | Decreases value by one step. |
Home | Jumps to min. |
End | Jumps to max. |
PageUp | Increases value by a larger step (browser-determined). |
PageDown | Decreases value by a larger step (browser-determined). |
Framework API
The props, events, and slots of the Vue component. The standalone CSS layer has no component API — its surface is the classes above. (Svelte bindings are planned.)
Props
| Prop | Type | Default | Description |
|---|---|---|---|
color | ThemeColor | 'primary' | Semantic accent: primary · secondary · success · warn · danger · info · surface. |
disabled | boolean | — | Sets native disabled on the input and data-disabled on the wrapper. |
label | string | — | Visible <label> text, linked to the input via for/id. |
max | number | 100 | Maximum value — maps to the native max attribute. |
min | number | 0 | Minimum value — maps to the native min attribute. |
modelValue | number | — | Controlled value; bind with v-model. Defaults to min when omitted. |
showValue | boolean | — | Renders the current value in an ori-slider__value span beside the label. |
step | number | 1 | Increment per arrow-key press — maps to the native step attribute. |
Events & attributes
| Event | Payload | Description |
|---|---|---|
update:modelValue | number | Fired live on every input (each drag tick / arrow press); drives v-model. |
change | number | Fired once when the value is committed — pointer release, or a keyboard step. Use it to record undo history or run a per-release side effect that must not fire per tick (see Commit on release). |
<OriSlider v-model="volume" label="Volume" :show-value="true" />
@change carries the committed number, not a DOM Event — it is a declared emit, so it no longer
falls through to the <input>. If you need the raw native event (e.g. event.target.validity), attach
a listener to the underlying <input> via a template ref.
OriSlider sets inheritAttrs: false, so undeclared native attributes (aria-label,
aria-describedby, name, id, …) fall through to the <input>, not the wrapper — they name,
describe, and submit the real control. label auto-generates the input id via useId(), so an
explicit id is only needed to override it.
Slots
| Slot | Description |
|---|---|
label | Rich label content (an icon + text, markup). Falls back to the label prop; keeps the for/id a11y wiring. |