Testing Automated accessibility testing, manual testing techniques, and screen reader testing
Automated tools catch about 30-50% of accessibility issues. Comprehensive testing requires a combination of automated scans, manual checks, and assistive technology testing.
Accessibility Testing Pyramid
├── Automated (CI/CD) — catch regressions fast
│ ├── Linters (eslint-plugin-jsx-a11y)
│ ├── Unit tests (jest-axe, Testing Library)
│ └── Integration tests (axe-core, Lighthouse CI)
├── Manual — catch what automation misses
│ ├── Keyboard navigation walkthrough
│ ├── Zoom and text spacing tests
│ └── Content and reading order review
└── Assistive Technology — validate real experience
├── Screen reader testing (NVDA, VoiceOver, JAWS)
├── Voice control testing (Dragon, Voice Control)
└── Magnification software testing
Catches common mistakes during development.
npm install --save-dev eslint-plugin-jsx-a11y
// .eslintrc.js
module . exports = {
extends: [ 'plugin:jsx-a11y/recommended' ],
plugins: [ 'jsx-a11y' ],
};
Common issues caught:
Images without alt attributes
Click handlers without keyboard handlers
Missing htmlFor on labels
Invalid ARIA attributes
Autofocus usage
npm install --save-dev jest-axe
import { render } from '@testing-library/react' ;
import { axe, toHaveNoViolations } from 'jest-axe' ;
expect. extend (toHaveNoViolations);
describe ( 'Button' , () => {
it ( 'has no accessibility violations' , async () => {
const { container } = render (
< button onClick = {() => {}} > Submit </ button >
);
const results = await axe (container);
expect (results). toHaveNoViolations ();
});
});
describe ( 'Form' , () => {
it ( 'has no accessibility violations' , async () => {
const { container } = render (
< form >
< label htmlFor = "email" > Email </ label >
< input id = "email" type = "email" required />
< button type = "submit" > Submit </ button >
</ form >
);
const results = await axe (container);
expect (results). toHaveNoViolations ();
});
});
Testing Library encourages accessible patterns by prioritizing queries that reflect how users interact with the page.
import { render, screen } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
// Query priority (most to least preferred):
// 1. getByRole — accessible role
// 2. getByLabelText — form fields
// 3. getByPlaceholderText — when no label
// 4. getByText — non-interactive text
// 5. getByDisplayValue — current input value
// 6. getByAltText — images
// 7. getByTitle — title attribute
// 8. getByTestId — last resort
test ( 'form submission' , async () => {
const user = userEvent. setup ();
render (< LoginForm />);
// Uses accessible queries
const emailInput = screen. getByRole ( 'textbox' , { name: / email / i });
const passwordInput = screen. getByLabelText ( / password / i );
const submitButton = screen. getByRole ( 'button' , { name: / sign in / i });
await user. type (emailInput, 'user@example.com' );
await user. type (passwordInput, 'password123' );
await user. click (submitButton);
expect (screen. getByRole ( 'alert' )). toHaveTextContent ( 'Welcome!' );
});
test ( 'dialog keyboard interaction' , async () => {
const user = userEvent. setup ();
render (< ConfirmDialog />);
// Open dialog
await user. click (screen. getByRole ( 'button' , { name: / delete / i }));
// Verify dialog is open
const dialog = screen. getByRole ( 'dialog' );
expect (dialog). toBeInTheDocument ();
// Close with Escape
await user. keyboard ( '{Escape}' );
expect (dialog).not. toBeInTheDocument ();
});
npm install --save-dev @axe-core/playwright
// Playwright + axe-core
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 })
. withTags ([ 'wcag2a' , 'wcag2aa' , 'wcag21aa' ])
. analyze ();
expect (results.violations). toEqual ([]);
});
test ( 'navigation is keyboard accessible' , async ({ page }) => {
await page. goto ( '/' );
// Tab through main navigation
await page.keyboard. press ( 'Tab' ); // Skip link
await page.keyboard. press ( 'Enter' ); // Activate skip link
// Verify focus moved to main content
const focused = await page. evaluate (() => document.activeElement?.id);
expect (focused). toBe ( 'main-content' );
});
// Scan specific page sections
test ( 'form section is accessible' , async ({ page }) => {
await page. goto ( '/contact' );
const results = await new AxeBuilder ({ page })
. include ( '#contact-form' )
. analyze ();
expect (results.violations). toEqual ([]);
});
// lighthouserc.js
module . exports = {
ci: {
collect: {
url: [ 'http://localhost:3000/' , 'http://localhost:3000/about' ],
numberOfRuns: 3 ,
},
assert: {
assertions: {
'categories:accessibility' : [ 'error' , { minScore: 0.9 }],
// Specific audits
'color-contrast' : 'error' ,
'document-title' : 'error' ,
'html-has-lang' : 'error' ,
'image-alt' : 'error' ,
'label' : 'error' ,
'link-name' : 'error' ,
'list' : 'error' ,
'meta-viewport' : 'error' ,
},
},
upload: {
target: 'temporary-public-storage' ,
},
},
};
# Run Lighthouse CI
npx lhci autorun
Check What to Verify Tab through entire page All interactive elements reachable, logical order Shift+Tab Reverse navigation works Enter/Space on buttons All buttons activate Escape on modals/popups Dialogs close, focus returns Arrow keys in widgets Tabs, menus, dropdowns navigate correctly Focus visibility Focus indicator always visible No keyboard traps Can always Tab away from any element Skip link Bypasses navigation, moves focus to main content
Test Expected Result Browser zoom to 200% Content reflows, no horizontal scroll Browser zoom to 400% Content remains readable in single column Text-only zoom (Firefox) Text enlarges without breaking layout Text spacing override Content remains visible, no overlap or clipping
Reading order matches visual order
Heading hierarchy is logical (no skipped levels)
Links have descriptive text (not just "click here" or "read more")
Error messages explain how to fix the problem
Color is not the sole indicator of information
Images have appropriate alt text
Screen Reader Platform Browser Usage NVDA Windows Firefox, Chrome Most popular free screen reader JAWS Windows Chrome, Edge Most popular commercial screen reader VoiceOver macOS Safari Built into macOS VoiceOver iOS Safari Built into iOS TalkBack Android Chrome Built into Android Narrator Windows Edge Built into Windows
Action Keys Turn on/off Cmd + F5Navigate next VO + Right Arrow (Ctrl + Option + Right)Navigate previous VO + Left ArrowActivate element VO + SpaceRead all VO + AOpen rotor (landmarks, headings) VO + UNavigate by heading VO + Cmd + H
Action Keys Turn on Ctrl + Alt + NStop speaking CtrlNavigate next Tab or Down ArrowNavigate headings HNavigate landmarks DList elements NVDA + F7Read current line NVDA + Up Arrow
Screen Reader Testing Checklist
├── Page Structure
│ ├── Page title is announced
│ ├── Headings create logical outline
│ ├── Landmarks are present and labeled
│ └── Language is correctly identified
├── Navigation
│ ├── Skip link works
│ ├── Navigation items are announced with count
│ ├── Current page is indicated (aria-current)
│ └── Focus management on route changes
├── Forms
│ ├── Labels announced with each input
│ ├── Required fields indicated
│ ├── Error messages announced
│ └── Groups (fieldset/legend) announced
├── Dynamic Content
│ ├── Live regions announce updates
│ ├── Modals trap focus and announce
│ ├── Loading states communicated
│ └── Status messages announced
└── Interactive Widgets
├── Custom components announce role, name, state
├── State changes announced (expanded, checked, selected)
├── Keyboard patterns work as expected
└── Disabled state communicated
Audit Workflow
├── 1. Automated Scan
│ ├── Run axe-core or Lighthouse on all pages
│ ├── Fix all automatically detected issues
│ └── Re-scan to confirm fixes
├── 2. Keyboard Audit
│ ├── Tab through every page
│ ├── Test all interactive components
│ └── Verify focus management
├── 3. Visual Review
│ ├── Check contrast with browser DevTools
│ ├── Test at 200% zoom
│ ├── Test with text spacing overrides
│ └── Test in forced-colors mode
├── 4. Screen Reader Audit
│ ├── Test with VoiceOver (Mac) or NVDA (Windows)
│ ├── Navigate by headings, landmarks, forms
│ └── Test dynamic content and notifications
└── 5. Report & Prioritize
├── Document issues by WCAG criterion
├── Rate severity (critical, serious, moderate, minor)
└── Create remediation plan with priorities
Severity Description Example Critical Blocks access for a user group No keyboard access, missing form labels Serious Major barrier, workaround exists Poor contrast, missing skip link Moderate Some difficulty, content still usable Missing landmark labels, ambiguous link text Minor Annoyance, doesn't block access Inconsistent focus styles, missing lang on inline text
Tool Type Use Case eslint-plugin-jsx-a11y Linter Static analysis during development jest-axe Unit test Component-level accessibility testing @axe-core/playwright E2E test Page-level accessibility scanning Lighthouse Audit Performance and accessibility scoring axe DevTools (browser extension) Manual Interactive accessibility inspection WAVE (browser extension) Manual Visual accessibility evaluation Colour Contrast Analyser Manual Color contrast checking (desktop app) Accessibility Insights Manual Guided manual assessment (Microsoft) pa11y CLI / CI Automated accessibility testing Storybook a11y addon Development Component accessibility testing in Storybook
Accessibility Testing Guidelines
Integrate automated testing (axe, Lighthouse) into CI/CD
Use Testing Library queries that enforce accessible patterns
Test keyboard navigation manually on every feature
Test with at least one screen reader before release
Audit contrast, zoom, and text spacing during design review
Prioritize fixes by severity — critical blockers first
Automate what you can, but never skip manual testing
Include accessibility in your Definition of Done