Accessible Price-Range Sliders: The WAI-ARIA Multi-Thumb Slider Pattern for E-Commerce Filters
Quietramp ·
Accessible Price-Range Sliders: The WAI-ARIA Multi-Thumb Slider Pattern for E-Commerce Filters
A “$0 — $200” price filter with two draggable handles on a single track is one of the
most common controls on a category or search-results page. It’s also almost never a
native HTML form control — there’s no built-in two-handle <input type="range"> — so
it’s built from scratch: a styled <div> track, two <div> or <span> thumbs, and a
pointermove listener that repositions whichever thumb the mouse grabbed. That build
path works fine for a mouse. It very often ships with no keyboard support at all.
Quick answer: a two-thumb price slider needs each thumb marked up individually with
role="slider", its own aria-valuenow/aria-valuemin/aria-valuemax, a distinct
accessible name (“Minimum price” and “Maximum price,” not the same “Price” label
twice), and full arrow-key operation — this is the WAI-ARIA APG’s own Multi-Thumb
Slider pattern, and its
own worked example is a two-thumb price-range filter. This is a keyboard-operability
and accessible-naming question (WCAG 2.1.1 and 4.1.2) — a separate failure mode from
whether the slider requires dragging versus a single click, which our article on
WCAG 2.2’s dragging-movements rule
already covers in depth.
This article covers technical accessibility practice, not legal advice — it explains what WCAG 2.1 AA requires for this component and how to meet it, not whether a specific implementation carries legal risk.
Two different questions about the same widget
It’s easy to conflate two separate requirements on a price-range slider, and conflating them is exactly how one gets fixed while the other ships broken. WCAG SC 2.5.7 Dragging Movements (Level AA, WCAG 2.2) asks: if this slider already works with a keyboard and a click, does operating it with a mouse or touchscreen also require a drag gesture? Its fix is a click-anywhere-on-the-track handler. That’s the question our dragging-movements article answers.
This article answers a different, earlier question: does the slider work with a
keyboard at all, and can a screen reader tell the two thumbs apart? A custom <div>
slider built purely from pointerdown/pointermove/pointerup listeners can fail this
one completely — Tab skips right past it, because there’s nothing in the tab order to
land on — regardless of whether anyone ever gets to the click-vs-drag question. That’s
WCAG SC 2.1.1 Keyboard
(Level A) territory, which states plainly: “All functionality of the content is
operable through a keyboard interface without requiring specific timings for individual
keystrokes.” A slider a mouse can move but a keyboard can’t is a textbook 2.1.1 failure,
independent of the drag question entirely.
The pattern: WAI-ARIA’s own example is a price-range slider
The Multi-Thumb Slider Pattern in the WAI-ARIA Authoring Practices Guide isn’t a generic pattern adapted to e-commerce — it’s written with this exact component in mind: “in a product search, a two-thumb slider could be used to enable users to set the minimum and maximum price limits for the search.” Its own reference example is titled “Horizontal Multi-Thumb Slider Example: Demonstrates a two-thumb slider for picking a price range for a hotel reservation.”
The pattern extends the base Slider Pattern, which requires:
role="slider"on each focusable thumb — not one role shared between both handles, one per thumb.aria-valuenow,aria-valuemin, andaria-valuemaxon each thumb, reflecting its current, minimum, and maximum allowed value.aria-valuetextwhen the raw number alone isn’t meaningful — for a price slider, this is where a$sign and comma formatting belong, so a screen reader announces “$45” instead of a bare “45.”- An accessible name per thumb, via
aria-labeloraria-labelledby— “Minimum price” and “Maximum price,” specifically, since the pattern requires each thumb be independently identifiable, not sharing one generic “Price” label between them.
The multi-thumb pattern adds one more requirement specific to a dependent range —
which a min/max price filter always is: “the thumbs are not allowed to pass one
another… the maximum value of the thumb that sets the lower end of the range is
limited by the current value of the thumb that sets the upper end of the range.”
Concretely, that means the min thumb’s aria-valuemax and the max thumb’s
aria-valuemin need to update live as the other thumb moves — the spec states this
directly: “the values of aria-valuemin or aria-valuemax of the dependent sliders
are updated when the value changes.” Skip this and a screen-reader user gets no
indication of why their minimum-price thumb suddenly refuses to go past $150 once the
maximum thumb sits at $150 — the visual thumbs stop moving, with nothing announced to
explain it.
Keyboard interaction, from the pattern’s own spec
Because a multi-thumb slider “implements the Slider Pattern,” each thumb gets the base pattern’s full keyboard model, verbatim:
| Key | Action |
|---|---|
| Right Arrow / Up Arrow | Increase the value by one step |
| Left Arrow / Down Arrow | Decrease the value by one step |
| Home | Set the slider to the minimum allowed value |
| End | Set the slider to the maximum allowed value |
| Page Up / Page Down (optional) | Larger increase/decrease than a single arrow-key step |
The pattern is also explicit about tab order stability: “each thumb is in the page tab sequence… the tab order remains constant regardless of thumb value and visual position within the slider.” In a dependent min/max pair, the minimum thumb should always be the first stop in the tab order and the maximum thumb the second, even if a user has dragged the minimum thumb visually past where the maximum thumb started — tab order tracks DOM position, not current pixel position.
What a real failure looks like
A common real-world build: two <div class="thumb"> elements positioned with
left: % inside a track <div>, each wired to pointerdown/pointermove listeners
that update the CSS position and an internal JavaScript variable — with no role, no
tabindex, and no keydown handler anywhere. Visually it works perfectly. A mouse
user drags either handle and the filtered results update. A keyboard user tabs from the
“Price” heading straight to the next filter group, because there’s nothing between them
to receive focus. WCAG SC 4.1.2 Name, Role, Value
(Level A) is the other SC this fails at the same time, since its own text requires
that “for all user interface components… the name and role can be programmatically
determined” — a <div> with no role attribute has no programmatically determinable
role at all, so even a screen reader user who somehow lands on it (via a virtual cursor
sweep, not Tab) hears nothing identifying it as a control.
A minimal fix pattern, layered onto an existing custom slider without a rebuild:
<div class="price-slider">
<div class="track"></div>
<div
class="thumb thumb-min"
role="slider"
tabindex="0"
aria-label="Minimum price"
aria-valuemin="0"
aria-valuemax="150"
aria-valuenow="0"
aria-valuetext="$0"
></div>
<div
class="thumb thumb-max"
role="slider"
tabindex="0"
aria-label="Maximum price"
aria-valuemin="0"
aria-valuemax="200"
aria-valuenow="200"
aria-valuetext="$200"
></div>
</div>
function onThumbKeydown(event, thumb, step, updateValue) {
const key = event.key;
const now = Number(thumb.getAttribute("aria-valuenow"));
const min = Number(thumb.getAttribute("aria-valuemin"));
const max = Number(thumb.getAttribute("aria-valuemax"));
let next = now;
if (key === "ArrowRight" || key === "ArrowUp") next = Math.min(now + step, max);
else if (key === "ArrowLeft" || key === "ArrowDown") next = Math.max(now - step, min);
else if (key === "Home") next = min;
else if (key === "End") next = max;
else return; // not a slider key -- let it pass through
event.preventDefault();
updateValue(next); // sets aria-valuenow/aria-valuetext and the other thumb's dependent min/max
}
The existing pointerdown/pointermove drag handling doesn’t need to change — this
just adds the keyboard path alongside it, and the same updateValue function both
paths call is also the place to update the other thumb’s aria-valuemin/
aria-valuemax per the dependent-range requirement above.
A five-minute manual test
Automated scanners can confirm a role="slider" element has the required ARIA
attributes present — axe-core’s aria-required-attr rule
(“Ensure elements with ARIA roles have all required ARIA attributes”) is the generic
check that would catch a slider missing aria-valuenow outright. It can’t confirm an
arrow-key press actually changes the value, and there’s no rule that flags two thumbs
sharing one ambiguous label — a slider with aria-label="Price" on both thumbs passes
every automated check while remaining genuinely unusable non-visually. A quick manual
pass catches both:
- Tab to the filter section. Confirm each thumb receives focus individually, in a fixed order, and announces a distinct name (“Minimum price” / “Maximum price”) plus its current formatted value.
- With a thumb focused, press the Right and Left arrow keys. Confirm the value changes and the filtered product list updates the same way a drag would.
- Push the minimum thumb’s value up against the maximum thumb’s current value. Confirm it stops there (doesn’t cross) and that the announced value reflects the actual stopping point, not a stale one.
Where this fits into a real audit
This is exactly the kind of interactive-component failure a single-pass automated scan structurally can’t catch — the markup can look completely correct while the keyboard path silently doesn’t exist. Quietramp’s $890 audits include a manual pass that actually operates components like this one with a keyboard, not just a parser reading the DOM for the right attribute names. See a real sample report or check pricing — $890 one-time for a full WCAG 2.1 AA audit, $99/month for ongoing re-checks after fixes ship.
FAQ
Do I need two separate <input type="range"> elements instead of a custom slider?
Not necessarily — two native <input type="range"> elements layered on the same track
is a legitimate, often simpler way to get keyboard support and ARIA semantics for free,
and it’s worth considering before building a fully custom widget. The pattern in this
article is for sites that already have a custom-built slider and need to retrofit
accessibility onto it without a full rebuild.
Does aria-valuetext replace aria-valuenow, or do I need both?
Both. aria-valuenow still needs the raw numeric value for assistive technology and
any code that reads slider state programmatically; aria-valuetext is an additional,
human-readable override of what gets announced — the formatted “$45” instead of the
bare number.
Is this the same issue as a filter checkbox missing a label? Related but distinct. A missing checkbox label (covered in our faceted-search article) is a static form-control naming problem. A slider thumb is a fully custom interactive widget with no native keyboard behavior at all until it’s built in — the naming problem is real here too, but it’s layered on top of a bigger gap.
Quietramp is an AI-operated agency with human oversight — this article was drafted by our Content/SEO writer role.