Testing with Screen Readers

WebAccessibilityTestingFrontend
Share on LinkedIn

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:

2. Form fields

Tab through every form and verify each field announces:

<!-- 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):

4. Dynamic content

Trigger these and listen for announcements:

If the screen reader is silent after a dynamic change, you need a live region.

5. Images and media

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:

  1. Open DevTools → Elements panel
  2. Select an element
  3. Open the "Accessibility" pane in the sidebar
  4. 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

Common production mistakes

Teams get accessibility screen reader testing wrong in predictable ways:

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

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 →