How do you actually verify a feature is accessible? What can automated tools catch, and what can they never catch?
A realistic testing strategy — knowing automated scans cover only a fraction and what manual testing adds.
A credible strategy layers automated and manual testing, because each catches what the other can't. AUTOMATED tools — axe-core (via jest-axe, Playwright, or the axe DevTools/Lighthouse UIs), or eslint-plugin-jsx-a11y at lint time — are fast, cheap, and belong in CI on every PR. But by axe's own estimate they detect only ~30–50% of WCAG issues: they reliably catch machine-checkable things — missing alt, missing form labels, insufficient color contrast, duplicate ids, invalid ARIA attribute/role combinations, missing document language, empty buttons/links. What they FUNDAMENTALLY CANNOT judge is meaning and experience: whether alt text is actually MEANINGFUL (not just present), whether the focus ORDER is logical, whether a custom widget's keyboard interactions work, whether an announcement makes sense in context, whether focus is managed on route change, whether content is understandable. Those require MANUAL testing: (1) KEYBOARD-only — unplug the mouse and Tab through the whole flow (reach everything, visible focus, logical order, no traps, Escape works). (2) SCREEN READER — exercise the feature with a real one (VoiceOver on macOS/iOS, NVDA or JAWS on Windows), which is the only way to hear what's actually announced. (3) ZOOM/REFLOW to 200%–400% and 320px width, and check with reduced-motion and forced-colors/high-contrast. (4) Include people with disabilities in usability testing where possible — the ground truth. So: automate to catch regressions cheaply and gate PRs, but never treat a green axe run as 'accessible' — the human tests are where real accessibility is confirmed.
Defining an a11y QA process; explaining why a passing axe scan isn't proof of accessibility.
// Automated: cheap regression gate in CI (catches ~30-50% of issues)
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
test('dialog has no automatically-detectable a11y violations', async () => {
const { container } = render(<Modal open title="Confirm">Body</Modal>);
expect(await axe(container)).toHaveNoViolations();
});
// What axe CANNOT verify (must be tested by a human):
// - is the alt text actually meaningful?
// - is the focus ORDER logical? does the focus trap work?
// - does the screen reader announce something that makes sense?
// - keyboard-only walkthrough, 200-400% zoom, reduced-motion, forced-colors