What screen readers hear, and the WCAG numbers
intermediate·8 min·Markdown for agents
What this lesson teaches
Announce changes with the quietest mechanism that works, hide things the right way, and cite the WCAG 2.2 thresholds that turn an opinion into a finding.
Below is a transcript of a reading an invoice aloud. It presses Save by itself once a lap; flip the mode, or press Save yourself mid-sentence.
Polite: “Changes saved” waits for the sentence to end. Right for success, counts and loading.
A sighted person sees "Saved" appear. A screen-reader user hears it only if something announces it, and how it is announced decides whether it helps or barges in.
The ladder: stop at the first rung that fits
Focus moves there anyway
An opened dialog, the first invalid field after Submit. Moving focus is the announcement. Add nothing.It belongs to one control
A field's error, a character count:aria-describedbyon the input, so it is read with the field.Not urgent, not tied to a control
A toast, "Saved", "12 results", "Loading…": a polite ,role="status".Urgent, not tied to a control
A form-level failure, a session about to expire:role="alert", and nothing else gets it.
role="status" is the polite one; role="alert" is assertive and interrupts. Default to polite. The common over-correction is an assertive success toast: "Saved" now cuts off whatever the user was listening to.
Making the region actually speak
Live regions are fussy. Three rules make them reliable:
- Render the region empty first, then change its text. A region that mounts already holding its message is announced inconsistently.
- Keep messages short and whole. The region re-reads everything inside it on each change.
- Never move focus to a toast. Announce it; leave focus where the person is working.
✕ Before
{msg && <div role="status">{msg}</div>}✓ After
<div role="status" className="sr-only">{msg}</div>For loads, put aria-busy="true" on the updating area, say "Loading…" politely, then the outcome: "Loaded, 12 results".
Hiding, two opposite ways
| You want | Use |
|---|---|
| Invisible on screen, still read aloud | .sr-only: 1px box, clip-path: inset(50%), white-space: nowrap. One pixel, not zero: some readers skip empty boxes |
| Visible on screen, not read aloud | aria-hidden="true" on decorative icons and visual duplicates |
Never put aria-hidden on or above a button, link or input. Tab still lands there, and the reader says nothing exists. To hide something interactive, use inert.
Alt text by purpose
| The image | alt |
|---|---|
| Decorative, or repeats nearby text | alt="": present and empty |
| Informative | What it adds: "Ticket QR code" |
| The image is the button | The action: "Search", not "magnifying glass" |
| A chart | A one-line summary, with the data in a table nearby |
A missing alt is worse than an empty one: the reader falls back to the file name.
The numbers that make a finding
In a review, "this feels inaccessible" is an opinion. A criterion with its number is a finding. WCAG 2.2 Level A and AA are the rules products are held to:
| Criterion | The rule |
|---|---|
| 1.4.3 Contrast | Text 4.5:1; large text (24px, or 18.67px bold) 3:1 |
| 1.4.11 Non-text contrast | Input borders, icons, focus rings: 3:1 against every adjacent color |
| 1.4.1 Use of color | Status never by color alone |
| 2.5.8 Target size | 24×24px, or spaced so a 24px circle on it touches no other target |
| 2.5.7 Dragging | Every drag has a single-click alternative |
| 2.4.11 Focus not obscured | The focused element is never fully hidden by sticky chrome |
| 1.4.4 / 1.4.10 | Survives 200% text; works at 320px wide with vertical scroll only |
| 2.2.2 Pause, stop, hide | Anything moving or updating for more than 5 seconds has a pause |
| 1.4.2 Audio | Nothing autoplays with sound |
Disabled controls and logos are exempt from contrast. The 44px touch target and "one <h1> per page" are good advice but not AA, so they are recommendations, never the top severity.
Audit this screen for screen-reader output and WCAG 2.2 AA: - Pick the quietest announcement per change: focus move, aria-describedby, role="status", role="alert" (failures that block only). - Live regions are always in the DOM and start empty; never focus a toast. - Decorative icons aria-hidden; nothing aria-hidden contains a focusable element. - Alt text by purpose; empty alt for decoration. - Report each failure as: element, WCAG criterion number, measured value, threshold. Don't change colors unless I ask.
Quick check
A 'Changes saved' toast should be announced with…
In the skill
The nodes your agent reads when it does this work.
- Screen-reader announcementsPick the announcement by a four-step ladder — focus move, aria-describedby, role="status", role="alert" — and stop at the first match. Live regions exist empty before their text changes; aria-busy around loads; alt text by purpose, not looks; sr-only boxes are 1px; aria-hidden never sits on or above anything focusable.
components/screen-reader-announcements - WCAG 2.2 thresholdsThe WCAG 2.2 A/AA numbers that turn a preference into a finding — 4.5:1 and 3:1 contrast, 3:1 focus rings, 24×24 targets, a single-pointer path for every drag, focus never hidden under sticky chrome, 200% text and 320px reflow, a pause on anything moving over 5 s. A finding cites one of these or a task an assistive-tech user cannot finish; 44px targets and heading outlines are recommendations.
components/wcag-thresholds
Connected lessons
What this lesson builds on and leads to, across every course.