Slider
stimeo--slider
A single-handle slider you can move by dragging or from the keyboard.
A slider with one handle. The current value is announced, and its position is readable from CSS as a fraction, so drawing the handle and the track is yours. The minimum, maximum, step and starting value come from your markup. It answers to both dragging and the keyboard.
Keyboard
| Key | Action |
|---|---|
| → / ↑ | Increase the value by one step. With logicalTrack under RTL the horizontal pair swaps. |
| ← / ↓ | Decrease the value by one step. With logicalTrack under RTL the horizontal pair swaps. |
| PageUp / PageDown | Move the value by ten steps. |
| Home / End | Jump to the minimum / maximum value. |
<%# Markup for the slider (APG Slider / single thumb) demo.
stimeo--slider provides aria-valuemin/max/now plus keyboard and drag operation, and
exposes the thumb position fraction (0..1) as the root CSS variable
--stimeo--slider-fraction. The thumb's actual placement is the Playground's CSS
using that variable. %>
<div
class="slider"
data-controller="stimeo--slider"
data-stimeo--slider-min-value="0"
data-stimeo--slider-max-value="100"
data-stimeo--slider-step-value="1"
data-stimeo--slider-value-value="40">
<%# The hidden field carries the value into the form; it is optional. %>
<input type="hidden" name="volume" data-stimeo--slider-target="field" />
<div class="slider__track" data-stimeo--slider-target="track"
data-action="pointerdown->stimeo--slider#onPointerDown">
<div class="slider__range" aria-hidden="true"></div>
<div class="slider__thumb" role="slider" tabindex="0"
aria-label="<%= t("components.slider.demo.label") %>"
data-stimeo--slider-target="thumb"
data-action="keydown->stimeo--slider#onKeydown"></div>
</div>
</div>
<%# Readout: what the component just reported. Purely visual — the component
already announces what a screen reader needs, so this line is aria-hidden to
keep it from being read twice. The value is a code fragment (API identifiers
and values), so only the label is translated. %>
<p class="demo-readout" aria-hidden="true">
<span><%= t("catalog.readout.field") %></span>
<span class="demo-readout__value" data-readout
data-empty-text="<%= t("catalog.readout.empty") %>">—</span>
</p>
/*
* Presentation-only styles for the slider demo.
* The thumb position is computed from the CSS variable the library exposes on the
* root: --stimeo--slider-fraction (0..1).
*/
.slider {
width: min(20rem, 100%);
padding: 0.75rem 0.625rem;
}
.slider__track {
position: relative;
height: 0.375rem;
border-radius: 999px;
background: var(--border);
}
/* Fill from 0 to the current value (the fraction) with the accent color. */
.slider__range {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: calc(var(--stimeo--slider-fraction, 0) * 100%);
border-radius: 999px;
background: var(--accent);
}
/* Place the thumb at the fraction position (translateX(-50%) to center it). */
.slider__thumb {
position: absolute;
top: 50%;
left: calc(var(--stimeo--slider-fraction, 0) * 100%);
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
background: var(--surface-card);
border: 2px solid var(--accent);
transform: translate(-50%, -50%);
cursor: grab;
}
.slider__thumb:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
// The hidden field mirrors the value, and for a move the user made it emits the
// native `change` a form already listens to — so `stimeo--auto-submit`, form
// validation, or any `change` handler on the `<form>` reacts without knowing what
// this component is. A refresh that follows a morph or a repair is silent, and
// the component reports a value the page moved — a runtime `min` / `max` /
// `step` / `value` change — as `stimeo--slider:reconcile`. This demo reads both
// events, so the readout follows the user's moves and the page's.
// The page chrome uses some of these controllers too (the nav, the tab strip),
// so everything below is scoped to the demo itself.
const demoStage = document.querySelector(".demo-stage") ?? document;
const fieldReadout = demoStage.querySelector("[data-readout]");
const fieldSources = demoStage.querySelectorAll("[data-stimeo--slider-target='field']");
// An input without a `name` is not a submitted control, so it is left out.
const submitted = () =>
Array.from(demoStage.querySelectorAll("[data-stimeo--slider-target='field']"))
.filter((input) => input.name)
.map((input) => `${input.name}=${input.value}`)
.join(" & ") || fieldReadout?.dataset.emptyText || "";
const showSubmitted = () => {
if (fieldReadout) fieldReadout.textContent = submitted();
};
for (const source of fieldSources) source.addEventListener("change", showSubmitted);
demoStage.addEventListener("stimeo--slider:reconcile", showSubmitted);
showSubmitted();
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--slider"
Targets
| Name | Description | Attribute |
|---|---|---|
track
required
|
The rail; a pointerdown sets the value from the press position and starts a drag. | data-stimeo--slider-target="track" |
thumb
required
|
The draggable handle (role=slider); its ARIA value reflects the current value. |
data-stimeo--slider-target="thumb" |
field
|
Optional hidden input the value is mirrored into; a native change fires only for a move the user made. |
data-stimeo--slider-target="field" |
Values
| Name | Description | Attribute |
|---|---|---|
min
|
Lowest value (default 0); the Home target. | data-stimeo--slider-min-value |
max
|
Highest value (default 100); the End target. | data-stimeo--slider-max-value |
step
|
Snap/step increment (default 1); arrows move by it, Page by 10×. | data-stimeo--slider-step-value |
value
|
Current value (default 0). One outside the range or off the step grid is clamped and snapped where it is published (ARIA, fraction, field); the value itself is left as written until the user moves the thumb. | data-stimeo--slider-value-value |
logicalTrack
|
Whether the track is laid out with logical CSS and mirrors under RTL (default false). When set, the pointer mapping and the horizontal arrow pair follow the writing direction; left unset nothing reads direction. | data-stimeo--slider-logical-track-value |
Actions
| Name | Description | Action |
|---|---|---|
onKeydown
|
Keyboard stepping per APG (Arrows, Page by 10×, Home/End to min/max). | stimeo--slider#onKeydown |
onPointerDown
|
Starts a pointer drag, setting the value from the pointer position. | stimeo--slider#onPointerDown |
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires when the user moves the value (keyboard or drag); detail { value }. |
stimeo--slider:change |
reconcile
|
Fires when the page moves the published value (a runtime min / max / step / value change or a morph), once per batch; same detail as change. Not fired on connect. |
stimeo--slider:reconcile |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-valuenow |
Thumb | The current value. |
aria-valuemin / aria-valuemax |
Thumb | The minimum and maximum of the range. |
--stimeo--slider-fraction |
Root element | The current value as a 0..1 fraction, used to position the thumb. |