Why jest-axe alone is never enough
A green jest-axe report does not mean a React application is actually usable with NVDA or VoiceOver. Automated tools find structural ARIA errors, but not confusing announcement order or illogical focus jumps. This guide shows how to build screen reader testing for React apps systematically across both layers.
Table of Contents
- 1. The gap between automated and real testing
- 2. Integrating jest-axe into the test suite
- 3. Anchoring automated checks in the CI pipeline
- 4. Manual testing with NVDA on Windows
- 5. Manual testing with VoiceOver on macOS
- 6. Building a repeatable test protocol
- 7. Prioritizing critical components
- 8. Adding screen reader signals to E2E tests
- 9. Test layers compared
- 10. Summary
- 11. FAQ
1. The gap between automated and real testing
A screen reader test for React apps that consists solely of automated linter checks uncovers, in practice, only a fraction of the actual usability problems. Tools such as jest-axe or eslint-plugin-jsx-a11y check structural rules: a missing alt attribute, insufficient color contrast, a missing form label. These are important but purely static checks that say nothing about how a component actually sounds when NVDA or VoiceOver reads it aloud.
A concrete example illustrates the gap: an accordion element can have every ARIA attribute set correctly and still sound confusing in practice, because the announcement order on opening is illogical or the expanded state is only communicated after a noticeable delay. Such problems can only be uncovered by actually listening with a screen reader, no automated test can replace that experiential layer.
The right approach for screen reader testing in React apps is therefore not an either/or choice, but a combination of three layers: automated rule checking on every pull request, structured manual passes with real screen readers before larger releases, and a fixed test protocol that ensures the same critical paths are consistently checked on every pass.
2. Integrating jest-axe into the test suite
jest-axe integrates the axe-core engine into Jest or Vitest and checks rendered components against an extensive rule base covering WCAG criteria. The big advantage: violations become visible right while new components are being written, long before a manual screen reader test even takes place. Integration happens through a simple matcher that can be dropped into any existing test file without changing the rest of the test structure.
It matters to run jest-axe not only against isolated components but also against composite views with multiple interacting elements, for example a complete form with several fields and an error summary. Isolated component tests often miss problems that only arise from the interplay of multiple elements, such as duplicate ids or conflicting aria-live regions on the same page.
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import RegistrationForm from './RegistrationForm';
expect.extend(toHaveNoViolations);
test('registration form has no accessibility violations', async () => {
const { container } = render(<RegistrationForm />);
const results = await axe(container, {
rules: {
// Disable color-contrast in jsdom, it lacks real rendering
'color-contrast': { enabled: false },
},
});
expect(results).toHaveNoViolations();
});
3. Anchoring automated checks in the CI pipeline
So that screen reader test relevant regressions do not only surface during review, jest-axe belongs as a fixed part of the CI pipeline, failing the build on every new violation. It matters to maintain the rule configuration centrally, so all teams apply the same thresholds instead of each team defining its own, diverging exceptions.
A proven pattern is a separate CI job that runs exclusively accessibility tests, with its result showing up as its own status check on the pull request, separate from the functional tests. This makes visible whether a change specifically introduces accessibility regressions, instead of that information getting lost in one large, mixed test report.
# .github/workflows/accessibility.yml
name: Accessibility Tests
on: [pull_request]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run test:a11y -- --ci
- name: Fail build on violations
run: npm run test:a11y -- --ci --reporters=default --reporters=jest-junit
4. Manual testing with NVDA on Windows
NVDA is the most widely used free screen reader on Windows and the de facto standard for manual screen reader testing of React applications. A sensible first step: use Insert+Down for browse mode navigation to check whether heading structure, landmarks, and form labels are read in the expected order. NVDA also offers an elements list (Insert+F7) that shows all links, headings, and form fields on a page at a glance, ideal for quickly spotting missing or duplicate labels.
For React specific test cases, it is especially worth checking dynamically loaded content, for example after a client side route change. NVDA does not announce a route change in a single page application by default unless the application actively moves focus to the new page heading or updates a live region with the new page title. Exactly this pattern, focus on <h1> after every route change, is one of the most common issues that turns up during NVDA passes of React single page apps.
5. Manual testing with VoiceOver on macOS
VoiceOver ships preinstalled on macOS and can be activated with Cmd+F5, making it the obvious second screen reader for screen reader testing of React apps, especially since announcement behavior differs between NVDA and VoiceOver in details. The VoiceOver rotor (Ctrl+Option+U) shows a structured overview of headings, links, and form elements similar to the NVDA elements list and is excellent for quickly reviewing a page's landmark structure.
One difference that often produces divergent behavior between the two screen readers in React applications concerns live region announcements: VoiceOver in some Safari versions reacts more sensitively to rapidly consecutive aria-live updates than NVDA in Chrome, which can lead to swallowed announcements that would have gone unnoticed in an NVDA test. That is why a single screen reader is not enough for solid screen reader testing, critical user flows should be checked with both combinations.
6. Building a repeatable test protocol
Without a fixed protocol, manual screen reader tests quickly become random and inconsistent, each tester checks different aspects, and regressions surface late. A repeatable protocol defines concrete test steps for every critical user flow, for example registration, checkout, or form validation, with clearly stated expectations: which announcement should follow which action, where focus should move, what order is correct.
The protocol should live as a markdown or table document in the repository, versioned along with the code itself, so new components automatically bring new test cases with them. An example entry: "Submit form with two invalid fields, expected: focus moves to the first invalid field, error summary is announced via role=alert, field name and error message are included in the announcement." This precision is the difference between an arbitrary ad hoc test and a solid screen reader test protocol.
// a11y-test-protocol.md (excerpt, kept alongside the component)
//
// Flow: Checkout form submission
// Steps:
// 1. Fill in invalid email, leave required address field empty
// 2. Submit the form
// Expected with NVDA (Insert+Down browse mode):
// - Focus moves to the email field
// - role="alert" summary announces "2 errors found"
// - Each field announces its own error via aria-describedby
// Expected with VoiceOver (Cmd+F5):
// - Same focus behavior
// - Rotor (Ctrl+Option+U) shows both invalid fields flagged
7. Prioritizing critical components
Not every component deserves the same amount of testing effort. For screen reader testing in React apps, it pays off to prioritize by usage frequency and criticality: checkout forms, login flows, and navigation menus should be manually checked with NVDA and VoiceOver on every major release, while rarely used settings pages can be included in the manual test cycle less often, for example quarterly.
Components with complex keyboard interaction such as date pickers, autocomplete fields, drag and drop lists, and multi step wizards deserve special attention, because these combine the most ARIA patterns and automated tools have the least to say here. A simple rule of thumb: the more keyboard interaction patterns a component implements at once, the higher the priority for a manual screen reader test.
8. Adding screen reader signals to E2E tests
Playwright and other E2E frameworks can read a page's accessibility tree and thereby partially verify what a screen reader would theoretically read aloud, without simulating a real screen reader. With page.accessibility.snapshot() or the newer getByRole API, you can check whether roles, names, and states arrive correctly in the accessibility tree, providing an additional, automatable layer between plain jest-axe and real manual testing.
This intermediate layer does not replace a real screen reader test, but it reduces the number of manual passes needed for every small change. A sensible workflow: jest-axe for every component, accessibility tree snapshots in E2E tests for critical flows, and full manual NVDA/VoiceOver passes only before larger releases or when changing especially complex, interactive components.
9. Test layers compared
The overview below arranges the different layers of screen reader testing for React apps by effort, frequency, and reliability, to help derive a sensible combination for your own project.
| Test layer | Frequency | Reliability | Effort |
|---|---|---|---|
| jest-axe per component | Every commit | Structural rules | Very low |
| Accessibility tree in E2E | Every pull request | Roles, names, states | Low |
| Manual NVDA test | Before release, critical flows | Very high | High |
| Manual VoiceOver test | Before release, critical flows | Very high | High |
| Test protocol review | Quarterly | Consistency over time | Medium |
None of these layers fully replaces another. Automated checks scale well and run on every commit, but only catch structural errors. Manual screen reader passes are more expensive, but find exactly the problems that make the difference between a frustrating and a working experience for real users.
Mironsoft
React development with a focus on accessibility and design systems
Does your app actually know how it sounds with NVDA?
We integrate jest-axe into your CI pipeline, build a test protocol for critical user flows, and run manual screen reader passes with NVDA and VoiceOver.
CI integration
Set up jest-axe and accessibility tree checks as a dedicated status check
Test protocol
Document critical flows and make them repeatably testable
Manual audits
NVDA and VoiceOver passes for the most important user flows
10. Summary
Solid screen reader testing for React apps comes from the interplay of several layers: jest-axe catches structural ARIA errors on every commit, accessibility tree checks in E2E tests add an automatable intermediate layer, and manual passes with NVDA and VoiceOver uncover exactly the problems no automated tool can detect, confusing announcement order, illogical focus jumps, or inconsistent behavior across different screen reader and browser combinations.
A fixed, versioned test protocol makes manual screen reader tests repeatable instead of random and ensures critical user flows like checkout and registration are consistently checked on every major release. Prioritizing by usage frequency and interaction complexity ensures that the limited manual testing effort is invested where it makes the biggest difference for real users.
Screen Reader Testing for React Apps — the essentials at a glance
Automated layer
jest-axe per component and in the CI pipeline, catches structural ARIA errors on every commit.
Manual layer
NVDA on Windows and VoiceOver on macOS for real usability problems before major releases.
Test protocol
Versioned document with concrete steps and expectations for critical user flows.
Prioritization
Checkout, login, and complex interaction patterns first, rare settings pages less often.