Accessible Slider & Range Input Guide
A slider is a value, not two states. This guide covers the native <input type="range"> that gives you role="slider", keyboard operation, and a click-to-set drag alternative for free; the full arrow / Home / End / Page keyboard contract; the aria-valuetextproperty that turns “2” into “Medium”; the 2.5.7 dragging requirement; and dual-thumb range sliders — with copy-ready code mapped to WCAG 2.2.
The Control With the Longest Keyboard Contract
A slider looks simple — a rail and a knob you slide — and it is the control developers most often rebuild from <div>s and mouse events, throwing away everything the browser already does. The result is a widget a mouse user can drag and nobody else can touch: no role, so a screen reader never calls it a slider; no value, so there is nothing to announce; no keyboard, so an arrow-key user is locked out; and no way to set it without a sustained drag, so anyone who cannot hold and move a pointer is stuck.
The slider has an ARIA pattern of its own — the WAI-ARIA Authoring Practices Slider pattern — built from role="slider" and the four value properties aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext. And unlike almost every other pattern, most of the time you get the whole thing for free from a single native element. Where a switch answers on-or-off with one key, a slider chooses a value from a range, so it owns the longest keyboard contract of any single control: two arrow directions, Home, End, and the Page keys.
This guide starts where you should start — with the native range input — then covers what it exposes, the keyboard model in full, the custom role="slider" build for when native is not enough, the single-pointer alternative that 2.5.7 Dragging Movements demands, dual-thumb range sliders, and the visual rules that decide whether the slider is usable once the semantics are right.
The shortcut most guides bury: reach for <input type="range"> first. It is announced as a slider, operable with every key below, settable by clicking the track, and it exposes its value automatically. You only take on the work in the later sections when the native element genuinely cannot do the job — most often for two thumbs on one track.
The WCAG 2.2 Criteria a Slider Must Satisfy
| Criterion | Level | What the slider must do |
|---|---|---|
| 1.3.1 Info & Relationships | A | The current value, the bounds, and the slider role are exposed programmatically, not implied by the position of a coloured bar alone. |
| 2.1.1 Keyboard | A | The value can be set with the arrow keys, Home, End, and the Page keys, with no pointer required. |
| 2.1.2 No Keyboard Trap | A | Tab moves focus to the slider and away again — the arrows adjust the value, they do not trap focus on the control. |
| 1.4.1 Use of Color | A | Where the value sits is shown by thumb position and text, not by a colour cue alone (for example a green-to-red track). |
| 2.4.7 Focus Visible | AA | The thumb shows a clearly visible focus indicator when tabbed to, distinct from its resting appearance. |
| 1.4.11 Non-text Contrast | AA | The thumb, the track, and the boundary between the filled and unfilled portions each reach at least 3:1 against adjacent colours. |
| 2.5.7 Dragging Movements | AA | The value can be set without dragging — a click on the track or plus/minus buttons — not by a sustained press-and-move only. |
| 2.5.8 Target Size (Minimum) | AA | The thumb is at least 24 by 24 CSS pixels so it can be operated by touch and imprecise pointers. |
| 4.1.2 Name, Role, Value | A | The control exposes the slider role, an accessible name, and an aria-valuenow (or aria-valuetext) that tracks the real value. |
2.5.7 Dragging Movements is the criterion that catches teams by surprise, because it is new in WCAG 2.2 and a slider is its clearest example. Keyboard support does not satisfy it — a single click that sets the value, with no drag, does. A native range input passes this automatically; a custom one does not until you add the alternative yourself.
1. Do You Even Need a Slider?
A slider is the right control for choosing an approximate value along a continuous or stepped range where the exact number is not critical — volume, brightness, a zoom level, a price band. It is a poor control when the user needs to enter a precisevalue, because hitting exactly “$1,437” by sliding is painful for everyone and nearly impossible for someone with a motor impairment. Choose the control that matches the task before you style anything.
Slider
An approximate value on a range where the feel matters more than the exact digit — volume, brightness, zoom. <input type="range"> or role="slider". This is what the guide covers.
Number input
A precise value the user knows and wants to type — a quantity, an exact price, an age. <input type="number"> in the forms guide. Pair it with a slider if you want both.
Spinbutton
A small set of discrete steps nudged up and down — a passenger count, a stepper. Plus/minus buttons around a value, exposed with role="spinbutton" or a native number input.
A powerful combination for precision-sensitive ranges is to pair a slider with a linked number field: the slider gives a quick, low-effort approximate set, and the number field lets a keyboard or screen reader user type the exact value. Keep the two in sync and give each its own accessible name. When in doubt, remember the slider's honest job is roughly here, not exactly this.
2. Start With <input type="range">
The native range input is to sliders what the native checkbox is to switches: a single element that arrives already accessible. The browser announces it as a slider, wires up every key in the keyboard table below, lets a mouse or touch user click anywhere on the track to jump the thumb, and maps its value, min, and max to aria-valuenow, aria-valuemin, and aria-valuemax for you. That last point also means it satisfies 2.5.7 Dragging Movements under the user-agent exception — the click-to-set behaviour is the browser's, not yours to re-earn.
<!-- A real label, and min/max/step/value on the input. -->
<label for="volume">Volume</label>
<input
type="range"
id="volume"
min="0"
max="100"
step="1"
value="50"
/>
<!-- Show the value as text for sighted users, kept in sync by script.
Do NOT put this in an aria-live region - the slider role already
announces the value on focus. -->
<output for="volume" id="volume-output">50</output>/* Give the thumb a real presence: >= 24x24 for target size (2.5.8),
and >= 3:1 contrast against the track for non-text contrast (1.4.11). */
input[type="range"] {
inline-size: 100%;
block-size: 1.5rem;
cursor: pointer;
}
/* A visible focus ring, distinct from the resting thumb (2.4.7). */
input[type="range"]:focus-visible {
outline: 3px solid #1d4ed8;
outline-offset: 4px;
}
/* Thumb + track can be styled per engine (::-webkit-slider-thumb,
::-moz-range-thumb). Keep the thumb ceil 24px and give the track a
visible edge so the fill boundary is not carried by colour alone. */This version has zero JavaScript for accessibility. The label names it, the browser gives it the slider role, every keyboard command, click-to-set, and the value mapping. Your script only reacts to the input event to update the visible <output>and apply the setting — it never manages the control's semantics. Styling range inputs used to be the reason people rebuilt them; modern ::-webkit-slider-thumb and ::-moz-range-thumb pseudo-elements make that largely unnecessary.
The honest limits of the native element: it is a single thumb (no built-in two-handle range), its tick marks via <datalist> are minimally styleable, and a genuinely non-linear scale (logarithmic price bands) needs script to map positions to values. Those, and only those, are the reasons to move to the custom build in section 6.
3. Anatomy: Roles, States, and Properties
A slider carries more value information than any other simple control, and that is where its ARIA lives. The rule: the element supplies the role, you (or the browser) supply the three required value properties, you add aria-valuetext whenever the number is not self-explanatory, and you always supply a name.
| Element / concept | Attribute | Purpose |
|---|---|---|
| The thumb / slider control | role="slider" (implicit on <input type="range">) | The focusable element the user operates. On a native range input the browser supplies the role; on a custom slider you add role="slider" and tabindex="0". |
| Current value | aria-valuenow | The numeric value now. Required on role="slider". Mapped from the value attribute for you on a native range input; set by hand on a custom one. |
| Range bounds | aria-valuemin / aria-valuemax | The lowest and highest values the slider allows. Required. On a dual-thumb slider these are updated dynamically so the two thumbs cannot cross. |
| Human-readable value | aria-valuetext | The value spoken to assistive technology when the raw number is not self-explanatory — "Medium", "$50", "Wednesday". Optional but often essential. |
| Accessible name | <label>, aria-labelledby, or aria-label | Names what the slider controls ("Volume", "Minimum price"). On a dual-thumb slider each thumb needs its own name. |
| Orientation | aria-orientation="vertical" | Only when the slider is vertical. Horizontal is the default and needs no attribute. |
| The track and filled portion | Decorative (CSS) | Presentational. Draw them with CSS; the value is carried by aria-valuenow, not by the width of a coloured bar alone. |
Note the three required properties on role="slider": aria-valuenow, aria-valuemin, and aria-valuemax. Omitting a bound is a common validity failure — the value has no context to be announced against. For how each role and property is surfaced to assistive technology, see the ARIA roles & attributes reference.
4. aria-valuetext: When the Number Isn't the Value
This is the property that separates a slider that merely passes an automated scan from one a blind user can actually use. When a slider's position maps to something other than a plain number, a screen reader reading the raw aria-valuenowannounces gibberish: it says “2” for a rating that means “Fair”, or “1500” for a price that means “$1,500”. aria-valuetext supplies the human string, and the screen reader announces it instead of the number.
<!-- A quality rating: the numbers 1-4 mean nothing spoken aloud. -->
<label id="quality-label">Quality</label>
<div
role="slider"
tabindex="0"
aria-labelledby="quality-label"
aria-valuemin="1"
aria-valuemax="4"
aria-valuenow="2"
aria-valuetext="Fair" <!-- announced INSTEAD of "2" -->
></div>// Keep aria-valuetext in step with aria-valuenow whenever the value moves.
const LABELS = ["Poor", "Fair", "Good", "Excellent"]
function setQuality(el, n) {
el.setAttribute("aria-valuenow", String(n))
el.setAttribute("aria-valuetext", LABELS[n - 1]) // "Fair"
}
// For a price slider, format the number rather than mapping to words:
// el.setAttribute("aria-valuetext", "$" + value.toLocaleString())The test: if you would not print the raw aria-valuenow number beside the slider as its visible value, you owe the user an aria-valuetext. When the number is the value — a percentage, a plain 0-to-100 volume — leave aria-valuetext off and let aria-valuenow speak for itself.
On a native <input type="range"> you can set aria-valuetext directly and update it in the input event handler — it is one of the few ARIA attributes it is correct to manage by hand on a native control, precisely because the browser cannot know that your “2” means “Fair”.
5. The Keyboard Model
This is the contract a native range input fulfils for free and a custom slider must implement in full. Every key here is expected; a slider that responds only to the arrow keys, with no Home, End, or Page support, is incomplete and slow to operate across a wide range.
| Key | Expected behavior |
|---|---|
| Right / Up arrow | Increases the value by one step. Up increases even on a horizontal slider, matching the native range input. |
| Left / Down arrow | Decreases the value by one step. On a vertical slider (aria-orientation="vertical") Up still increases and Down still decreases. |
| Home | Jumps to the minimum value (aria-valuemin). |
| End | Jumps to the maximum value (aria-valuemax). |
| Page Up | Increases by a larger step — commonly 10% of the range — so a user can cross a wide scale quickly. |
| Page Down | Decreases by the same larger step. |
| Tab / Shift + Tab | Moves focus to the slider and away from it. A single-thumb slider is one Tab stop; the arrows do the adjusting. |
The detail custom builds trip on is event.preventDefault(): without it, the arrow keys and the Page keys scroll the page instead of moving the thumb, so the slider is effectively inoperable by keyboard under 2.1.1 Keyboard. On the native element this is handled for you. For the wider contract every custom control owes, see the keyboard accessibility guide.
6. Building a Custom Slider
Reach for a custom slider only when the native range input truly cannot do the job — most often a two-handle range, or a non-linear scale. You take on the role, the value properties, the whole keyboard model, and the single-pointer alternative. Here is the shape of a conformant single-thumb build.
<label id="zoom-label">Zoom</label>
<div class="slider" id="track">
<div
class="slider__thumb"
role="slider"
tabindex="0"
aria-labelledby="zoom-label"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="40"
></div>
</div>const track = document.getElementById("track")
const thumb = track.querySelector('[role="slider"]')
const MIN = 0, MAX = 100, STEP = 1, PAGE = 10
function setValue(v) {
const value = Math.min(MAX, Math.max(MIN, v))
thumb.setAttribute("aria-valuenow", String(value))
thumb.style.insetInlineStart = ((value - MIN) / (MAX - MIN)) * 100 + "%"
applyZoom(value) // apply the effect
}
// The full keyboard contract. preventDefault stops the page scrolling.
thumb.addEventListener("keydown", (e) => {
const now = Number(thumb.getAttribute("aria-valuenow"))
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 + PAGE; break
case "PageDown": next = now - PAGE; break
case "Home": next = MIN; break
case "End": next = MAX; break
default: return // let other keys through
}
e.preventDefault() // <- without this the page scrolls
setValue(next)
})
// The single-pointer alternative (2.5.7): click the track to jump there.
track.addEventListener("pointerdown", (e) => {
const rect = track.getBoundingClientRect()
const ratio = (e.clientX - rect.left) / rect.width
setValue(Math.round(MIN + ratio * (MAX - MIN)))
thumb.focus()
})Three details are load-bearing. The role="slider" and tabindex="0" go on the thumb, the element the user operates, not the track. Every branch of the keydown handler calls preventDefault so the keys move the thumb rather than scrolling. And the pointerdown handler on the track is not a nicety — it is what satisfies 2.5.7 Dragging Movements, giving a mouse or touch user a way to set the value with a single click and no drag. Add drag on top of it if you like, but the click must exist.
7. The Single-Pointer Alternative (2.5.7 Dragging Movements)
A slider is the canonical draggable control, which puts it squarely under 2.5.7 Dragging Movements — new in WCAG 2.2. The requirement is precise: anything you can do by dragging must also be doable with a single pointer action that involves no dragging. Some users cannot hold a button and move at the same time; a tremor, a switch device, or a head pointer makes a sustained drag impossible.
Passes 2.5.7
A native range input (the browser gives click-to-set); or a custom slider where a single click on the track jumps the thumb to that point, or plus/minus buttons step the value. No sustained press-and-move is required.
Fails 2.5.7
A custom slider whose thumb can only be dragged — click the track and nothing happens. Adding keyboard support does not fix it; that is 2.1.1, a different requirement for a different group of users.
The trap here is assuming a keyboard alternative covers it. It does not: 2.5.7 is about pointer users who cannot drag, and a keyboard is not a pointer. The fix is small — the pointerdown-on-track handler from section 6, or a pair of stepper buttons — but it must be there. See the full 2.5.7 Dragging Movements guide for the essential and user-agent exceptions and other draggable patterns.
8. Dual-Thumb Range Sliders
The two-handle range — a minimum-and-maximum price filter is the everyday example — is the single most common reason to leave the native input behind, because <input type="range"> has only one thumb. The accessible model is simple to state: build two separate sliders that share one track, each its own focusable role="slider" with its own value and its own name.
<div class="range" id="price">
<!-- Minimum thumb: its max is capped at the maximum thumb's value. -->
<div
role="slider"
tabindex="0"
aria-label="Minimum price"
aria-valuemin="0"
aria-valuemax="800" <!-- = current value of the max thumb -->
aria-valuenow="200"
aria-valuetext="$200"
></div>
<!-- Maximum thumb: its min is floored at the minimum thumb's value. -->
<div
role="slider"
tabindex="0"
aria-label="Maximum price"
aria-valuemin="200" <!-- = current value of the min thumb -->
aria-valuemax="1000"
aria-valuenow="800"
aria-valuetext="$800"
></div>
</div>Two rules make it work. First, give each thumb a distinct accessible name — “Minimum price” and “Maximum price” — so a screen reader user always knows which handle has focus. Second, keep the thumbs from crossing with dynamic bounds: every time either thumb moves, set the minimum thumb's aria-valuemaxto the maximum thumb's current value, and the maximum thumb's aria-valueminto the minimum thumb's current value. Each thumb is its own Tab stop, so the user tabs to one, adjusts it with the arrows, tabs to the other, and adjusts that.
This is the clearest case to not hand-roll. Two constrained sliders sharing state, staying in order, each dragging, clicking, and keyboarding correctly, is a lot of surface area to get right. A well-tested headless library — see section 9 — implements the pattern and its edge cases so you do not re-discover them in production.
9. Making the Value Visible — Contrast and Target Size
Once the semantics are right, a slider fails visually — and it fails for the users least able to absorb it. Three rules keep the value readable and operable for everyone.
- Give the thumb, track, and fill real edges. The thumb against its background, and the boundary between the filled and unfilled portions of the track, each need 3:1 contrast under 1.4.11 Non-text Contrast. A pale thumb on a pale track, or two close greys for filled and unfilled, is the usual miss. A 1px border rescues it.
- Make the thumb big enough to hit. The thumb is the target, and it must be at least 24 × 24 CSS pixels for 2.5.8 Target Size — aim for 44 on touch. A 10px handle looks elegant and is unusable for many.
- Show the value, and never in colour alone. Print the current value as text next to the slider, and do not encode meaning in track colour by itself — a green-to-red “risk” track is invisible to a colour-blind user unless the value is also spelled out, which is 1.4.1 Use of Color.
Verify the real rendered colours — not the values in the design file — with the contrast checker, and confirm the thumb size on a real device with the mobile accessibility checker. Remember any visible value text is held to the stricter 1.4.3 Contrast (Minimum) at 4.5:1.
10. Sliders in React
For a single-thumb slider in React, the native range input is still the safest base: a controlled <input type="range"> whose value comes from state and whose onChange updates it. React keeps the DOM value in sync, so aria-valuenow stays correct automatically — you only add aria-valuetext when the number is not the value.
import { useId, useState } from "react"
const LABELS = ["Poor", "Fair", "Good", "Excellent"]
function QualitySlider() {
const [value, setValue] = useState(2)
const id = useId()
return (
<div>
<label htmlFor={id}>Quality</label>
<input
id={id}
type="range"
min={1}
max={4}
step={1}
value={value}
// The number 1-4 is meaningless spoken, so give it words.
aria-valuetext={LABELS[value - 1]}
onChange={(e) => setValue(Number(e.target.value))}
/>
{/* Visible value for sighted users - NOT in an aria-live region. */}
<output htmlFor={id}>{LABELS[value - 1]}</output>
</div>
)
}For a two-handle range, non-linear scales, or fully custom visuals, do not hand-roll the role="slider" logic — reach for a headless library that has solved the edge cases: Radix UI's Slider(supports multiple thumbs), React Aria's useSlider / useSliderThumb, or Headless UI. The same principles travel to other frameworks — see the Vue and Angular guides, and the React accessibility guide for the surrounding patterns. Whatever you ship, verify it against the workflow below rather than trusting the README.
How to Test an Accessible Slider
Automated tools catch a missing name, a missing bound, or a role that never reached the element. Everything that decides whether the slider is actually usable — the keyboard contract, the value announcement, the drag alternative — takes a few minutes by hand.
- Work the keyboard. Tab to the slider (visible focus), then arrow keys change the value by one step, Home and End jump to the bounds, and Page Up / Page Down make a larger jump. The page must not scroll while you do it.
- Listen with a screen reader. You should hear the name, the word “slider”, and the current value — and where the raw number is not meaningful, the
aria-valuetextstring (“Fair”, “$200”) instead. The screen reader testing guide has the commands. - Click the track. With the mouse, click a point on the track and confirm the thumb jumps there with no drag — your 2.5.7 check.
- Measure the visuals. Thumb, track, and fill boundary reach 3:1 (1.4.11); the thumb is at least 24 × 24 px (2.5.8); the value shows as text, not colour alone (1.4.1).
- Check for a double-announce. If moving the slider makes the screen reader say the value twice, you have wrapped it in a stray
aria-liveregion — remove it and let the slider role announce.
Layer automated checks on top with axe-core, and see automated vs manual testing for where each fits. Scan the live page with the URL accessibility auditor to catch a missing name, role, or value before it ships.
Common Slider Mistakes & How to Fix Them
| Anti-pattern | Why it fails | The fix |
|---|---|---|
| A <div> with an onMouseDown that moves a thumb, and no role. | Assistive technology sees no control — no slider role, no value, no keyboard. A screen reader user never learns the slider exists or what it is set to (4.1.2, 2.1.1). | Use <input type="range">, or a <div role="slider"> with tabindex, aria-valuenow/min/max, and a full keydown handler. |
| role="slider" with aria-valuenow but no aria-valuemin or aria-valuemax. | The slider announces a value with no context — "70" out of what? Both bounds are required for the role to be valid and meaningful (4.1.2). | Always set aria-valuemin and aria-valuemax alongside aria-valuenow. |
| A rating slider announces "2" for a position that means "Fair". | The raw number is meaningless or misleading to a screen reader user, who cannot see the labels the number maps to (1.3.1, 4.1.2). | Add aria-valuetext="Fair" so the human-readable value is spoken instead of the number. |
| A custom slider you can drag but cannot set by clicking the track. | A mouse or touch user who cannot perform a sustained drag has no way to set the value. Keyboard support does not cover this (2.5.7). | Let a single click on the track jump the thumb there, or add plus/minus buttons — the same alternative a native range gives for free. |
| The slider value is wrapped in an aria-live region so it is announced. | The slider role already announces value changes on focus, so the live region double-announces — noisy and confusing (4.1.3 misuse). | Remove the live region; keep the value in aria-valuenow / aria-valuetext and let the slider role speak. |
| A thin thumb and a pale track, filled and unfilled in two close greys. | Low-vision users cannot find the thumb or read where the value sits; the boundaries fall below 3:1 (1.4.11). | Give the thumb, the track, and the filled/unfilled boundary at least 3:1, and make the thumb at least 24×24 (2.5.8). |
| Arrow keys scroll the page instead of moving the thumb. | A custom slider that does not call preventDefault on the arrow keys is effectively inoperable by keyboard (2.1.1). | In the keydown handler, handle the arrow / Home / End / Page keys and call event.preventDefault() for each. |
Accessible Slider Checklist
- Right control. The task is an approximate value on a range. If the user needs a precise number, offer a linked number field too.
- Native first.
<input type="range">with a real label, unless you genuinely need two thumbs or a non-linear scale. - Role and value.
role="slider"witharia-valuenow,aria-valuemin, andaria-valuemax— all three (4.1.2). - Readable value.
aria-valuetextwherever the raw number is not the value (“Medium”, “$50”). - Full keyboard. Arrows, Home, End, Page Up, Page Down all work, and the page never scrolls (2.1.1).
- Drag alternative. A single click on the track, or stepper buttons, sets the value without dragging (2.5.7).
- Visible and reachable. Thumb, track, and fill reach 3:1 (1.4.11); the thumb is at least 24 × 24 px (2.5.8).
- No double-announce. The value is not wrapped in a redundant
aria-liveregion — the slider role announces it.
Work through the full WCAG 2.2 checklist to see the slider in the context of every other requirement.
Check Your Sliders on a Live Page
Scan any page with our free axe-core-powered auditor to catch a slider with no accessible name, a missing bound, or a value that never reaches assistive technology — then run the keyboard, track-click, and contrast passes above for the failures no scanner can see.
Frequently Asked Questions
What is the difference between a slider and a range input?▾
They are two names for the same control at different layers. "Slider" is the ARIA role — role="slider" — that describes a widget for choosing a value from within a range by moving a thumb. "Range input" is the HTML element, <input type="range">, that the browser renders as a slider and exposes with that same role automatically. So every native range input is a slider, but not every slider is a range input: you can also build one from a plain element with role="slider" and manage its value yourself. The practical advice is to start with the native range input, because it gives you the slider role, keyboard operation, and a click-to-set pointer alternative without any JavaScript, and only drop to a custom role="slider" widget when the native element genuinely cannot do what you need.
How do I make an accessible slider in HTML?▾
Use <input type="range"> with a real <label>, and set min, max, step, and value. That single element is announced as a slider, is operable with the arrow keys, Home, End, Page Up and Page Down, can be set by clicking anywhere on the track, and exposes its value to assistive technology through aria-valuenow, aria-valuemin, and aria-valuemax for you. If the numeric value would not make sense read aloud on its own — a rating of "2" that really means "Fair", or "50" that means "$50" — add aria-valuetext with the human-readable version. Only build a custom slider from a div with role="slider" when you need behaviour the native element cannot provide, such as two thumbs on one track, and in that case you take on the keyboard handling and the click-to-set alternative yourself.
Which keys operate a slider?▾
A slider has the richest keyboard contract of any single control. The Right and Up arrows increase the value by one step; the Left and Down arrows decrease it by one step. Home jumps to the minimum and End jumps to the maximum. Page Up and Page Down move by a larger step — typically ten percent of the range or a defined jump — so a user does not have to press an arrow a hundred times to cross a wide scale. A native range input implements all of these for you. If you build a custom slider you must add every one of them in a keydown handler, and you must call preventDefault so the arrow keys move the thumb instead of scrolling the page. The slider itself is a single Tab stop: Tab moves to it, the arrows adjust it, and Tab moves away.
What is aria-valuetext and when do I need it?▾
aria-valuetext supplies a human-readable string for the slider's current value, and a screen reader announces it in place of the raw number in aria-valuenow. You need it whenever the number on its own does not communicate the value. A slider whose positions mean Small, Medium and Large should announce "Medium", not "2". A price slider should announce "$1,500", not "1500". A slider that picks a day should announce "Wednesday", not "3". Without aria-valuetext the screen reader reads the bare number, which is often meaningless or even misleading. The rule of thumb: if you would not print the raw aria-valuenow number next to the slider as its visible label, you owe the user an aria-valuetext. When the number genuinely is the value — a percentage, a plain 0-to-100 volume — you can leave aria-valuetext off and let aria-valuenow speak for itself.
Does a slider need a live region to announce its value?▾
No, and adding one is a common mistake. When a slider has focus and its aria-valuenow or aria-valuetext changes, screen readers announce the new value automatically as part of the slider role — that is exactly what the role is for. If you also place the value inside an aria-live region, the change is announced twice, which is noisy and confusing. Keep the value on the slider through aria-valuenow and aria-valuetext, show it visibly next to the control for sighted users, and let the slider role do the announcing. The one time a status message helps is when moving the slider changes something elsewhere on the page that is not the slider's own value — a total price recalculating, results filtering — and even then you announce that downstream change, not the slider value itself.
How does a slider satisfy WCAG 2.5.7 Dragging Movements?▾
WCAG 2.5.7, new in WCAG 2.2, requires that any action you can perform by dragging can also be performed with a single pointer action that involves no dragging — a plain click or tap. A slider is the textbook draggable control, so it is squarely in scope. A native <input type="range"> passes automatically, because the browser lets you click anywhere on the track to move the thumb there and the dragging behaviour is provided by the user agent. The moment you build a custom slider from divs and pointer events you lose that exception and must add the single-pointer alternative yourself: let a click on the track jump the thumb to that position, or provide plus and minus buttons that step the value. Note that keyboard support alone does not satisfy 2.5.7 — that is a separate requirement, 2.1.1 Keyboard. A mouse or touch user who cannot perform a sustained drag still needs a click-based way to set the value.
How do I build an accessible two-handle range slider?▾
A dual-thumb range slider — the kind used for a minimum-and-maximum price filter — is built as two separate sliders that share one track. Each thumb is its own focusable element with role="slider", its own aria-valuenow, and its own accessible name, such as "Minimum price" and "Maximum price", so a screen reader user knows which handle they are moving. The trick that keeps the two from crossing is dynamic bounds: the lower thumb's aria-valuemax is set to the current value of the upper thumb, and the upper thumb's aria-valuemin is set to the current value of the lower thumb, updated every time either one moves. Each thumb is a single Tab stop, so the user tabs to the minimum handle, adjusts it with the arrows, tabs to the maximum handle, and adjusts that. Because this involves state that must stay consistent across two constrained sliders, it is the clearest case for reaching for a well-tested headless library rather than hand-rolling it.
How do I test a slider for accessibility?▾
Start with the keyboard. Tab to the slider and confirm it takes focus with a visible indicator. Press the arrow keys and confirm the value changes by one step in the expected direction; press Home and End and confirm it jumps to the minimum and maximum; press Page Up and Page Down and confirm a larger jump. Then listen with a screen reader: you should hear the slider's name, that it is a slider, and its current value — and if the raw number is not meaningful, you should hear the aria-valuetext string instead. Next, put the mouse down: click a point on the track and confirm the thumb jumps there without a drag, which is your 2.5.7 check. Finally, look at the visuals — confirm the thumb, the track, and the filled portion each reach 3:1 contrast against what they sit on, that the thumb is at least 24 by 24 CSS pixels, and that the current value is shown as visible text and not conveyed by colour alone.
Essential Accessibility Resources
Comprehensive tools, checklists, and guides to help you create inclusive digital experiences