Forms & Input
Slider (Multi-Thumb)
AvailableRange selection such as price ranges. Key considerations include thumb interaction constraints and labeling.
What is a multi-thumb slider?
A multi-thumb slider has two thumbs that let users select a range (lower bound to upper bound). It is commonly used for filtering by price range, age range, time range, and similar criteria.
The challenge lies in giving each thumb its own distinct label, value, and range, and preventing the lower bound from exceeding the upper bound.
Why does accessibility matter?
- Screen reader users: If the two thumbs share the same (or no) label, users cannot tell which thumb they are operating or whether it controls the minimum or maximum. Each thumb must have a distinct label (e.g., "Minimum price" and "Maximum price").
- Keyboard-only users: Each thumb must be individually focusable and operable with arrow keys. Additionally, if the lower bound could cross the upper bound, the values would break, so a crossing constraint is required.
Live demo (recommended implementation)
Below is a two-thumb slider for selecting a price range.Each thumb is independently focusable and can be moved with the arrow keys. The minimum cannot exceed the maximum.
Try it: Tab to 'Minimum price' then 'Maximum price'. Use ← → to adjust by $500, Page Up / Down by $2,000, Home / End to jump to the limits. The minimum thumb cannot cross the maximum.
Tip
When you focus a thumb, a screen reader announces something like "Minimum price, slider, $2,000" or "Maximum price, slider, $6,000," making it clear which thumb you are on and its current value. The key technique is using aria-label to distinguish between the two thumbs.
Keyboard interaction
| Key | Action | Priority |
|---|---|---|
| Tab | Move between thumbs (min price and max price) | Required |
| → / ↑ | Increase the focused thumb by one step | Required |
| ← / ↓ | Decrease the focused thumb by one step | Required |
| Home / End | Move the thumb to its minimum or maximum limit | Required |
| Page Up / Page Down | Increase or decrease by a larger step | Recommended |
Required WAI-ARIA roles and properties
| Target | Attribute / Role | Meaning |
|---|---|---|
| Each thumb | role="slider" | Assign a slider role to each thumb (both thumbs if there are two). |
| Each thumb | tabindex="0" | Make each thumb individually keyboard-focusable. |
| Each thumb | aria-label (for each) | Provide a distinguishing label such as "Minimum price" or "Maximum price". |
| Each thumb | aria-valuenow | Current value of the thumb. Must be updated on every change. |
| Lower-bound thumb | aria-valuemax = current value of upper bound | Dynamically constrain the range so the lower bound cannot exceed the upper bound. |
| Upper-bound thumb | aria-valuemin = current value of lower bound | Dynamically constrain the range so the upper bound cannot go below the lower bound. |
| Each thumb | aria-valuetext | Provide a human-readable value with units such as "2000 yen" (optional but recommended). |
Implementation: recommended pattern (Good)
Good / Recommended
The native range input only supports a single thumb, so a two-thumb slider is built with two elements using role="slider". The key is giving each thumb a distinct aria-label and adynamically constrained range.
Markup:
<div class="range" id="price-range">
<span id="price-label">Price range ($)</span>
<!-- Minimum thumb -->
<div role="slider"
tabindex="0"
aria-label="Minimum price"
aria-valuemin="0"
aria-valuemax="6000" <!-- Up to the max thumb's current value -->
aria-valuenow="2000"
aria-valuetext="$2,000"
data-thumb="min"></div>
<!-- Maximum thumb -->
<div role="slider"
tabindex="0"
aria-label="Maximum price"
aria-valuemin="2000" <!-- From the min thumb's current value -->
aria-valuemax="10000"
aria-valuenow="6000"
aria-valuetext="$6,000"
data-thumb="max"></div>
</div>Keyboard handling and the "thumbs cannot cross" constraint:
const MIN = 0, MAX = 10000, STEP = 500, BIG = 2000;
const root = document.getElementById('price-range');
const minThumb = root.querySelector('[data-thumb="min"]');
const maxThumb = root.querySelector('[data-thumb="max"]');
function val(t) { return Number(t.getAttribute('aria-valuenow')); }
function render() {
// Clamp each thumb's range to the other's current value so they can't cross
minThumb.setAttribute('aria-valuemin', String(MIN));
minThumb.setAttribute('aria-valuemax', String(val(maxThumb)));
maxThumb.setAttribute('aria-valuemin', String(val(minThumb)));
maxThumb.setAttribute('aria-valuemax', String(MAX));
[minThumb, maxThumb].forEach((t) => {
t.setAttribute('aria-valuetext', '$' + val(t).toLocaleString());
t.style.setProperty('--pos', ((val(t) - MIN) / (MAX - MIN)) * 100 + '%');
});
}
function onKey(t) {
return (e) => {
const lo = Number(t.getAttribute('aria-valuemin'));
const hi = Number(t.getAttribute('aria-valuemax'));
const now = val(t);
let next = now;
switch (e.key) {
case 'ArrowRight': case 'ArrowUp': next = now + STEP; break;
case 'ArrowLeft': case 'ArrowDown': next = now - STEP; break;
case 'PageUp': next = now + BIG; break;
case 'PageDown': next = now - BIG; break;
case 'Home': next = lo; break;
case 'End': next = hi; break;
default: return;
}
e.preventDefault();
// Clamp to valid range so min never exceeds max
t.setAttribute('aria-valuenow', String(Math.min(hi, Math.max(lo, next))));
render();
};
}
minThumb.addEventListener('keydown', onKey(minThumb));
maxThumb.addEventListener('keydown', onKey(maxThumb));
render();Anti-pattern (Bad)
Below is a slider with two unlabeled thumbs that only respond to mouse dragging.There is no way to tell which is the lower or upper bound, and it cannot be operated with a keyboard.
Try it: You cannot Tab to the thumbs. A screen reader cannot distinguish between the two thumbs, and neither the value nor range is conveyed.
<!-- ❌ Anti-pattern: unlabeled two-thumb slider, mouse-only -->
<div class="track">
<div class="knob" onmousedown="drag(event, this)"></div>
<div class="knob" onmousedown="drag(event, this)"></div>
</div>
<!--
There is no way to tell which thumb is "minimum" and which is "maximum."
Without role / aria-valuenow, the value and range are not announced.
Without tabindex, the thumbs cannot be reached by keyboard.
-->Bad / Avoid
Problems with this implementation:
- Thumbs are indistinguishable — No
aria-label, so "minimum" and "maximum" cannot be told apart. - Not keyboard accessible — No
tabindex, so neither thumb can receive focus or respond to arrow keys. - Value and range are not conveyed — No
role="slider"oraria-valuenow. - No crossing constraint — The minimum can exceed the maximum without restriction, corrupting the values.
Implementation checklist
- Applied role="slider" to each of the two thumbs
- Each thumb has a distinguishing aria-label (e.g. "Minimum price", "Maximum price")
- Each thumb is individually focusable with tabindex="0"
- Thumbs can be moved with Arrow, Home, End, and Page Up/Down keys
- Lower bound cannot exceed upper bound (range is dynamically constrained by the other thumb's value)
- aria-valuenow is updated on every change
- Focus is visible (focus ring is not suppressed)
Source (English):Slider (Multi-Thumb) Pattern — W3C APG(opens in a new tab)