Accessibility Testing
Combine automated scans, manual keyboard testing, and screen reader testing to find a11y issues before users do.
Why
Accessibility testing is a layered approach because no single method finds all issues. Automated tools (axe-core, Lighthouse) catch ~30% of violations — the mechanical ones like missing alt text and low contrast. Manual keyboard testing catches another ~40% — the operability issues like unreachable buttons and trapped focus. Screen reader testing catches the remaining ~30% — the semantic issues like illogical heading order and unannounced dynamic content. Skipping any layer leaves a gap, and the gap is where the real users are excluded. A team that runs only axe-core and ships with zero violations has an app that is 30% accessible — the keyboard user still cannot Tab to the custom dropdown, and the screen reader user still cannot understand the form. The full layered approach is the only way to verify the app is usable by everyone.
When
Automated in every PR (CI), manual keyboard before every release, screen reader for major features and before launch. Full audit annually or on major redesign.
How
1. Automated testing in CI (catches ~30% — the mechanical issues) - axe-core in Playwright tests: run on every page after navigation - eslint-plugin-jsx-a11y: lint-time checks in React - Lighthouse CI: a11y audit in the build pipeline - @axe-core/playwright for integration tests - These catch: missing alt, no label, low contrast, invalid ARIA, duplicate IDs 2. Manual keyboard testing (catches ~40% — the operability issues) - Put the mouse away, navigate with Tab, Shift+Tab, Enter, Space, Escape, Arrows - Can you reach every interactive element? Is the focus order logical? - Is the focus indicator visible? Can you operate every widget? - Can you complete every task (form, checkout, navigation) with keyboard only? - This takes 10-30 minutes per page and finds the issues axe cannot 3. Screen reader testing (catches ~30% — the semantic issues) - NVDA (free) + Chrome/Firefox on Windows - VoiceOver + Safari on Mac - TalkBack on Android, VoiceOver + Chrome on iOS - Navigate by headings (H), forms (F), links (K), regions (R) - Can you understand the page structure? Are forms announced with labels? - Are dynamic changes announced? Are errors clear and recoverable? 4. User testing with disabled users (the gold standard) - Recruit users who actually use assistive technology (blind, motor-impaired) - Observe them completing real tasks (signup, checkout, search) - Their experience reveals issues no developer testing finds - Even 3-5 users reveal the major barriers 5. Track and fix findings - File each finding as a ticket with the WCAG criterion and the testing method - Prioritize by user impact (blocking a task > annoying > cosmetic) - Fix blockers before launch, track others in the backlog
Example
// 1. Automated: axe-core in Playwright (CI)
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test("checkout flow has no a11y violations", async ({ page }) => {
await page.goto("/checkout");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(results.violations).toEqual([]);
});
// 2. Manual keyboard test (before every release)
// Checklist:
// [ ] Tab through the page — every interactive element reachable?
// [ ] Focus indicator visible on every element?
// [ ] Tab order follows the visual reading order?
// [ ] Can open/close modal with keyboard? Focus returns to trigger?
// [ ] Can operate every dropdown, tab, accordion with arrows?
// [ ] Can complete the checkout flow with keyboard only?
// 3. Screen reader test (NVDA + Chrome)
// Checklist:
// [ ] Press H — heading structure is logical (one h1, then h2s)?
// [ ] Press F — form fields announced with their labels?
// [ ] Press K — link text is descriptive (not "click here")?
// [ ] Submit form with error — error announced?
// [ ] Add to cart — cart update announced (aria-live)?
// [ ] Navigate to new page — new page announced (focus moved)?
// 4. User testing script
// Task: "Sign up for an account and make a purchase"
// Observe: where does the screen reader user get stuck?
// Common findings: form error not announced, focus lost on submit,
// custom dropdown not operable, heading order illogical
// 5. Track findings
// Ticket: "WCAG 3.3.1: Form error not announced on signup (NVDA)"
// Priority: Blocker (user cannot complete signup)
// Method: Screen reader testing, 2026-01-15Common Problems
- ⚠Running only axe-core and treating zero violations as "accessible" — axe catches 30% of issues, and the other 70% (keyboard operability, screen reader semantics) require manual testing that the team skips.
- ⚠Testing with a screen reader once, finding it too hard, and never doing it again — the first screen reader test is overwhelming, but even 30 minutes reveals the major barriers, and it gets easier with practice.
- ⚠Doing a11y testing at the end of the project "when we have time" — by then, the heading structure, focus order, and contrast are baked into the design, and retrofitting is 10x harder than designing for a11y from the start.
- ⚠Not involving disabled users in testing — developer testing with a screen reader finds some issues, but real users reveal the barriers that developers do not notice because developers know how the app is supposed to work.
Best Practices
- ✓Run axe-core in CI on every PR, manual keyboard testing before every release, and screen reader testing for major features — the layered approach catches what each individual method misses.
- ✓Integrate a11y into the design phase (contrast, heading structure, focus indicators) rather than retrofitting at the end — designing for a11y is 10x cheaper than fixing it later.
- ✓Test with real disabled users at least once per major release — their experience reveals barriers that no automated or developer testing finds, and even 3-5 users find the major issues.
- ✓Track a11y findings as tickets with the WCAG criterion, the testing method, and the user impact — this makes the work visible, prioritized, and verifiable, not an afterthought.