Range Slider
stimeo--range-slider
A two-thumb range slider with mutually constrained handles.
The stimeo--range-slider controller implements the WAI-ARIA Multi-Thumb Slider pattern. Two thumbs (start and end) constrain each other so the start never crosses the end; each thumb's live bound is exposed via aria-valuemin/max, and the range fractions are published to CSS as --stimeo-range-start and --stimeo-range-end (0..1) so the consumer can position the thumbs and the selected range. Min, max, step, and the initial start/end are set through data-*-value attributes. The library provides behavior only.
Keyboard
| Key | Action |
|---|---|
| → / ↑ | Increase the focused thumb by one step (never past the other thumb). |
| ← / ↓ | Decrease the focused thumb by one step (never past the other thumb). |
| PageUp / PageDown | Move the focused thumb by ten steps. |
| Home / End | Jump the focused thumb to its movable minimum / maximum. |
<%# Markup for the range-slider (APG Slider, multi-thumb) demo.
stimeo--range-slider handles the two thumbs' (start / end) mutual constraint
(lower ≤ upper), keyboard / drag operation, and dynamic updates of each thumb's
aria-valuemin/max/now, and exposes the range as the root CSS variables
--stimeo-range-start / --stimeo-range-end (0..1). The thumb and range placement
is the Playground's CSS using those variables. %>
<div
class="range-slider"
data-controller="stimeo--range-slider"
data-stimeo--range-slider-min-value="0"
data-stimeo--range-slider-max-value="100"
data-stimeo--range-slider-step-value="1"
data-stimeo--range-slider-start-value="20"
data-stimeo--range-slider-end-value="80">
<div class="range-slider__track" data-stimeo--range-slider-target="track"
data-action="pointerdown->stimeo--range-slider#onPointerDown">
<div class="range-slider__range" aria-hidden="true"></div>
<div class="range-slider__thumb" role="slider" tabindex="0"
aria-label="<%= t("components.range_slider.demo.min_label") %>"
data-stimeo--range-slider-target="startThumb"
data-action="keydown->stimeo--range-slider#onKeydown"></div>
<div class="range-slider__thumb" role="slider" tabindex="0"
aria-label="<%= t("components.range_slider.demo.max_label") %>"
data-stimeo--range-slider-target="endThumb"
data-action="keydown->stimeo--range-slider#onKeydown"></div>
</div>
<output class="range-slider__output" data-range-slider-output aria-live="polite">20 – 80</output>
</div>
/*
* Presentation-only styles for the range-slider demo.
* The thumb and range positions are computed from the CSS variables the library
* exposes on the root: --stimeo-range-start / --stimeo-range-end (0..1).
*/
.range-slider {
width: min(22rem, 100%);
padding: 0.75rem 0.625rem;
}
.range-slider__track {
position: relative;
height: 0.375rem;
border-radius: 999px;
background: var(--border);
}
/* Fill from start to end with the accent color. */
.range-slider__range {
position: absolute;
top: 0;
height: 100%;
left: calc(var(--stimeo-range-start, 0) * 100%);
width: calc((var(--stimeo-range-end, 1) - var(--stimeo-range-start, 0)) * 100%);
border-radius: 999px;
background: var(--accent);
}
/* Place each thumb at its fraction position (translateX(-50%) to center it). */
.range-slider__thumb {
position: absolute;
top: 50%;
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
background: var(--surface-card);
border: 2px solid var(--accent);
transform: translate(-50%, -50%);
cursor: grab;
}
.range-slider__thumb[data-stimeo--range-slider-target="startThumb"] {
left: calc(var(--stimeo-range-start, 0) * 100%);
}
.range-slider__thumb[data-stimeo--range-slider-target="endThumb"] {
left: calc(var(--stimeo-range-end, 1) * 100%);
}
.range-slider__thumb:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.range-slider__output {
display: block;
margin-top: 0.75rem;
font-variant-numeric: tabular-nums;
color: var(--text-muted, var(--color-text-muted));
}
// Demo that subscribes to the range-slider change event and reflects the selected range
// into a readout. The library provides behavior only, so the value display is the
// consumer's (this script's) job.
document.querySelectorAll('[data-controller~="stimeo--range-slider"]').forEach((root) => {
const output = root.querySelector('[data-range-slider-output]');
if (!output) return;
root.addEventListener('stimeo--range-slider:change', (e) => {
output.textContent = `${e.detail.start} – ${e.detail.end}`;
});
});
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--range-slider"
Targets
| Name | Description | Attribute |
|---|---|---|
track
required
|
The rail; a pointerdown on it moves the nearest thumb and starts a drag. | data-stimeo--range-slider-target="track" |
startThumb
required
|
The lower (minimum) thumb (role=slider); capped by the end thumb. |
data-stimeo--range-slider-target="startThumb" |
endThumb
required
|
The upper (maximum) thumb (role=slider); capped by the start thumb. |
data-stimeo--range-slider-target="endThumb" |
Values
| Name | Description | Attribute |
|---|---|---|
min
|
Lowest value of the range (default 0). | data-stimeo--range-slider-min-value |
max
|
Highest value of the range (default 100). | data-stimeo--range-slider-max-value |
step
|
Snap/step increment (default 1); arrows move by it, Page by 10×. | data-stimeo--range-slider-step-value |
start
|
Initial lower thumb value (default 0); kept ≤ end. | data-stimeo--range-slider-start-value |
end
|
Initial upper thumb value (default 100); kept ≥ start. | data-stimeo--range-slider-end-value |
Actions
| Name | Description | Action |
|---|---|---|
onKeydown
|
Keyboard stepping for the focused thumb (Arrows, Page, Home/End), never crossing. | stimeo--range-slider#onKeydown |
onPointerDown
|
Starts a track drag, moving whichever thumb is nearest the press. | stimeo--range-slider#onPointerDown |
Events
| Name | Description | Event |
|---|---|---|
change
|
Dispatched on user-driven changes, with start and end in detail. |
stimeo--range-slider:change |
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 |
Each thumb | That thumb's current value. |
aria-valuemin / aria-valuemax |
Each thumb | The movable range, dynamically limited by the other thumb. |
--stimeo-range-start / --stimeo-range-end |
Root element | Each value as a 0..1 fraction, used to position the thumbs and range. |