How to Test Accessibility: Automation, Manual Checks and a Checklist
What automated tools catch and what they miss, tests with jest-axe and Playwright, a 15-minute manual check, screen reader commands and a pre-release checklist

Part 8 of 8 in the "Accessible Web in Practice" series. Previous part: Accessibility in React: IDs, Focus on Route Change and Announcements.
Automated checks find just over half of accessibility issues. The final part of the series covers a three-layer testing setup, what to check by hand in 15 minutes, and a checklist worth running before every release.
How to test
Automation: necessary but not enough
Automated tools don't find everything. Deque, the makers of axe-core, analyzed audit data (over 13,000 pages and nearly 300,000 issues) and found that automated tests caught 57.4% of issues by volume (Deque). The rest — whether alt text makes sense, whether focus order is logical, whether errors are understandable, how widgets behave — needs a human.
So a sensible setup has three layers: a linter while writing code, automated tests in CI, and manual checks of key user flows.
Browser tools:
- Lighthouse (built into Chrome DevTools) — a quick score covering a subset of axe rules;
- axe DevTools — Deque's extension, more detailed than Lighthouse;
- WAVE — WebAIM's extension, shows issues visually right on the page;
- Accessibility panel in DevTools — the accessibility tree, computed names and roles.
Unit tests. Testing Library nudges you toward accessibility by design: getByRole and getByLabelText find elements the way a screen reader does. If a test can't find a button with getByRole('button', { name: 'Send' }), neither can a user.
// Vitest/Jest + Testing Library + jest-axe
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe, toHaveNoViolations } from 'jest-axe';
import { expect, test } from 'vitest';
import { ContactForm } from './ContactForm';
expect.extend(toHaveNoViolations);
test('form has no axe violations', async () => {
const { container } = render(<ContactForm />);
expect(await axe(container)).toHaveNoViolations();
});
test('error is linked to the field and focus moves to it', async () => {
const user = userEvent.setup();
render(<ContactForm />);
await user.click(screen.getByRole('button', { name: 'Send' }));
const email = screen.getByLabelText('Email address');
expect(email).toHaveAttribute('aria-invalid', 'true');
expect(email).toHaveAccessibleDescription(/@ sign/);
expect(email).toHaveFocus();
});
(toHaveAccessibleDescription and toHaveFocus come from @testing-library/jest-dom.)
End-to-end. Checking whole pages in Playwright with @axe-core/playwright:
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('home page has no WCAG A/AA violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
test('modal: focus inside, Esc closes, focus returns', async ({ page }) => {
await page.goto('/');
const opener = page.getByRole('button', { name: 'Book a consultation' });
await opener.click();
const dialog = page.getByRole('dialog', { name: 'Book a consultation' });
await expect(dialog).toBeVisible();
await expect(dialog.getByLabel('Name')).toBeFocused();
await page.keyboard.press('Escape');
await expect(dialog).toBeHidden();
await expect(opener).toBeFocused();
});
A 15-minute manual check
For each key user flow (placing an order, sending a request, finding contact info):
- Keyboard only. Complete the flow without a mouse. Is focus visible at every step? Is the order logical? Any traps? Do menus and dialogs open and close?
- 200% and 400% zoom. Nothing overlaps, no horizontal scrolling, nothing is cut off.
- Structure. Look at the list of headings and landmarks (an extension or the screen reader's rotor) — does the page make sense from those alone?
- Screen reader. Go through the flow with at least one screen reader. The minimum command set:
| VoiceOver (macOS) | NVDA (Windows) | |
|---|---|---|
| Turn on | Cmd + F5 |
Ctrl + Alt + N |
| Next item | Ctrl + Option + → |
↓ |
| Next heading | Ctrl + Option + Cmd + H |
H |
| Elements list (rotor) | Ctrl + Option + U |
Insert + F7 |
| Activate | Ctrl + Option + Space |
Enter |
Ask yourself: does every element have a clear name? Are changes announced (errors, items added to cart)? Do I know where I am?
- Display modes. Turn on
prefers-reduced-motion,forced-colorsand dark mode through DevTools emulation (Rendering).
Test with people
No checklist replaces a user who works with a screen reader or voice control every day. If the product matters, include such people in usability testing.
Pre-release checklist
Structure and semantics
-
<html lang="...">is set; passages in other languages are marked up - A unique
<title>on every page - Landmarks:
header,nav(labeled if there are several),main(one),footer - Headings reflect structure, no skipped levels
- Buttons are
<button>, links are<a href>; no clickabledivs
Images and media
- Informative images have meaningful
alt, decorative ones havealt="" - Icon buttons and icon links have an accessible name
- Videos have captions, audio has a transcript; no autoplay with sound
Forms
- Every field has a visible
<label>connected to it - Radio and checkbox groups use
fieldset+legend -
autocompleteon fields with personal data - Errors: text next to the field,
aria-invalid,aria-describedby, focus on the first error - Paste is allowed in password and code fields
Keyboard and focus
- The whole flow works with a keyboard, no traps
- Focus is always visible and high-contrast; no
outline: nonewithout a replacement - There's a "Skip to main content" link
- Sticky elements don't cover focus (
scroll-padding-top) - Modals: focus inside,
Esccloses, focus returns - No
tabindexgreater than 0
Visual
- Text contrast ≥ 4.5 : 1 (large text ≥ 3 : 1), components and focus ≥ 3 : 1
- Information isn't conveyed by color alone
- The page works at 320px width and 200–400% zoom
- Zoom isn't disabled in
meta viewport -
prefers-reduced-motionandforced-colorsare handled - Targets ≥ 24×24px (44×44 is better on mobile)
Dynamic content
- Important changes are announced through a live region that exists in advance
- Route changes in SPAs update the
titleand move focus - ARIA states (
aria-expanded,aria-pressed...) stay in sync with the UI
Testing
- No
jsx-a11ylint errors - No axe violations (in tests or the extension)
- Key flows checked with a keyboard and at least one screen reader
Sources and further reading
Standards and references
- WCAG 2.2 and the WCAG Quick Reference — W3C
- ARIA Authoring Practices Guide — widget patterns and keyboard models
- Using ARIA — rules for applying ARIA
- MDN: Accessibility — HTML, ARIA and practices reference
- web.dev: Learn Accessibility — a free course from Google
Research and data
- WebAIM Million 2026 — yearly analysis of a million websites
- WebAIM Screen Reader User Survey — how people actually use screen readers
- Deque: Automated Testing Coverage
- WHO: Disability and health
Practice and blogs
- WebAIM Articles — in-depth articles on every topic
- The A11Y Project — community checklist and articles
- Adrian Roselli — deep dives into patterns and their support
- Sara Soueidan — accessible design and front-end, SVG
- Scott O'Hara — native elements and ARIA
- GOV.UK Design System — components tested with real users
- Microsoft Inclusive Design
Tools
- axe-core and @axe-core/playwright
- eslint-plugin-jsx-a11y
- jest-axe
- WAVE and the WebAIM Contrast Checker
- NVDA — a free screen reader for Windows
Accessibility isn't a separate feature or a final audit — it's a habit, like responsive design. Start small: field labels, visible focus, contrast and real buttons cover most of what trips users up. The rest comes with practice.
The "Accessible Web in Practice" series
- Web Accessibility: Who It Really Serves and Where to Start
- Semantic HTML and Alt Text: The Foundation of an Accessible Website
- Accessible Forms: Labels, Hints, Errors and Autofill
- Keyboard and Focus: Building a Website You Can Use Without a Mouse
- Contrast, Motion and Button Size: Visual Accessibility
- ARIA and Dynamic Content: When Attributes Help and When They Hurt
- Accessibility in React: IDs, Focus on Route Change and Announcements
- How to Test Accessibility: Automation, Manual Checks and a Checklist — you are here
Agama Labs builds websites and web applications. Want to check how accessible your site is? Write to us: hello@agamalabs.com


