Testing with Screen Readers
The form passed Lighthouse accessibility with a score of 98. I opened it in VoiceOver and heard: "Edit text, blank. Edit text, blank. Button." Three unlabeled fields and a submit button with no context. The automated scan checked for aria-label attributes and found them — on the wrapper divs, not the actual inputs. Labels existed in the visual design but weren't programmatically associated. This is the gap automated tools can't close. Screen reader testing is the only way to verify that what you built is what assistive technology users actually experience.
Screen reader basics
Screen readers convert the browser's accessibility tree into speech (or braille). They don't read the visual page — they read the semantic structure: roles, names, states, and properties.
What screen readers announce for a well-built button:
"Submit order, button"
What they announce for a poorly-built one:
"Click here, button" (vague label) "div, clickable" (missing role) "" (silence — element not in accessibility tree)
Setting up for testing
VoiceOver (macOS)
Enable: Cmd + F5 (or System Settings → Accessibility → VoiceOver)
Navigate: VO + arrow keys (VO = Control + Option)
Interact: VO + Shift + Down (enter a group)
Stop: VO + Shift + Up (exit a group)
Read all: VO + A
Rotor: VO + U (navigate by headings, links, form controls)
NVDA (Windows, free)
Download: nvaccess.org
Navigate: Arrow keys, Tab
Read all: NVDA + Down
Elements list: NVDA + F7
Stop speech: Ctrl
Browse/focus mode: NVDA + Space (toggle)
TalkBack (Android)
Enable: Settings → Accessibility → TalkBack
Navigate: Swipe right (next), swipe left (previous)
Activate: Double tap
Reading menu: Swipe up then right
What to test on every feature
1. Page structure
Open the rotor (VoiceOver) or elements list (NVDA) and verify:
- One
<h1>per page - Headings form a logical hierarchy (no skipped levels)
- Landmarks present:
main,nav,header,footer - Skip link works and moves focus to main content
2. Form fields
Tab through every form and verify each field announces:
- Its label ("Email address, edit text")
- Its type (text, password, combobox)
- Its state (required, invalid, disabled)
- Error messages when validation fails
<!-- Screen reader hears: "Email address, edit text, required" -->
<label for="email">Email address</label>
<input id="email" type="email" required aria-describedby="email-hint">
<span id="email-hint">We'll never share your email</span>
<!-- On error, hears: "Email address, invalid data, Enter a valid email" -->
<input id="email" aria-invalid="true" aria-describedby="email-error">
<span id="email-error" role="alert">Enter a valid email</span>
3. Interactive widgets
For each custom widget (tabs, menus, modals, carousels):
- Role announced correctly
- State changes communicated (expanded/collapsed, selected)
- Keyboard operation works (see keyboard navigation patterns)
- Focus moves logically on open/close
4. Dynamic content
Trigger these and listen for announcements:
- Form submission success/error
- Search results loading and updating
- Toast notifications appearing
- Content loaded via AJAX/fetch
- Sort/filter changes on data tables
If the screen reader is silent after a dynamic change, you need a live region.
5. Images and media
- Informative images: alt text announced
- Decorative images: not announced (empty alt or role="presentation")
- Charts/graphs: text alternative or data table available
- Video: captions available, player controls labeled
Common failures I find
| What you see | What VoiceOver says | Fix |
|---|---|---|
| Icon button (trash) | "Button" | Add aria-label="Delete item" |
| Custom dropdown | "Clickable" | Use <select> or add combobox ARIA pattern |
| Loading spinner | (silence) | Add role="status" with "Loading" text |
| Modal opens | Focus behind modal | Trap focus, use <dialog> |
| Table sort | (silence) | Add aria-sort and live region |
| Placeholder as label | "Edit text, blank" | Use <label>, not just placeholder |
Inspecting the accessibility tree
Before testing with a screen reader, inspect the tree in Chrome DevTools:
- Open DevTools → Elements panel
- Select an element
- Open the "Accessibility" pane in the sidebar
- Check: Name, Role, Properties, Computed properties
If the name is empty or the role is "generic," fix it before reaching for the screen reader.
Automated + manual workflow
PR opened
→ CI runs axe-core (catches ~30% of issues)
→ Developer fixes automated failures
→ Developer tests new/changed flows with VoiceOver (15 min)
→ Reviewer spot-checks with NVDA if Windows-specific
→ Merge
For CI, use @axe-core/playwright or jest-axe:
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
Automated scans catch missing alt text, color contrast, and invalid ARIA. Screen reader testing catches wrong labels, missing announcements, and confusing navigation.
Building team habits
- Keep VoiceOver enabled on one test machine in the office
- Add "screen reader tested" to your PR checklist
- Record a 2-minute VoiceOver walkthrough for complex features and attach to the PR
- Rotate screen reader testing across the team so it's not one person's burden
- When a bug is reported by an assistive technology user, add a test case that catches it
Common production mistakes
Teams get accessibility screen reader testing wrong in predictable ways:
- Skipping failure-mode rehearsal — run a game day or fault injection exercise before peak traffic, not after the first outage.
- Missing correlation context — every error path should carry request, trace, or tenant identifiers so incidents are debuggable.
- Optimizing for demo, not steady state — load tests, cache warm-up, and cold-start paths matter more than local dev latency.
- Undocumented trade-offs — if you chose speed over strict correctness (or vice versa), write that down for the next engineer.
Production implementations of accessibility screen reader testing fail when staging mirrors production topology poorly, rollback is untested, and on-call runbooks describe the happy path only.
Resources
- WebAIM screen reader testing guide
- VoiceOver user guide (Apple)
- NVDA user guide
- axe-core Playwright integration
- WCAG 2.1 understanding documents
Frequently asked questions
Which screen readers should I test with?
Test with at least two screen readers on different platforms to catch engine-specific behavior. VoiceOver on macOS or iOS and NVDA on Windows cover the two most common user populations. TalkBack on Android is essential if you have mobile web traffic. Each screen reader interacts with the browser's accessibility tree differently, so passing one does not guarantee passing another. JAWS on Windows is also widely used in enterprise but NVDA is free and catches most of the same issues.
What should I listen for during screen reader testing?
Listen for: element roles announced correctly (button, link, heading), labels read for every form field, state changes announced (expanded, checked, selected), dynamic content updates communicated via live regions, logical reading order matching visual layout, and no unexpected silence where content should be announced. Pay special attention to custom widgets — if the screen reader says 'group' or 'clickable' instead of the expected role, your ARIA or HTML semantics are wrong.
How often should I run screen reader tests?
Run a manual screen reader pass on every new interactive component or flow before it ships. Automated accessibility scans (axe, Lighthouse) should run on every PR in CI, but they catch only about 30 percent of issues that screen readers expose. A 15-minute VoiceOver walkthrough of a new feature catches naming, state, and navigation problems that no automated tool will find. Schedule a full screen reader audit quarterly for the entire application.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →