Accessibility Testing
Automated Testing Setup
axe-core: Use AxePuppeteer with .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) for automated audits. Exclude non-audited regions with .exclude().
jest-axe: Use toHaveNoViolations() matcher in component tests.
CLI Tools
npx @axe-core/cli https://example.com
npx pa11y https://example.com --standard WCAG2AA --threshold 0
lighthouse https://example.com --only-categories=accessibility
CI/CD: Run npx pa11y against localhost in CI. Gate PRs on zero WCAG2AA violations.
Manual Screen Reader Testing
Testing Priority
Minimum: NVDA + Firefox (Win), VoiceOver + Safari (Mac), VoiceOver + Safari (iOS)
Full: + JAWS + Chrome (Win), TalkBack + Chrome (Android)
VoiceOver (macOS) Commands
VO = Ctrl + Option
Toggle: Cmd + F5
Next/Prev element: VO + Right/Left Arrow
Enter/Exit group: VO + Shift + Down/Up
Read all: VO + A
Activate: VO + Space
Rotor: VO + U (navigate by headings, links, forms, landmarks)
Next heading: VO + Cmd + H
Next form control: VO + Cmd + J
Next link: VO + Cmd + L
NVDA (Windows) Commands
NVDA modifier = Insert
Say all: NVDA + Down Arrow
Elements list: NVDA + F7
Quick keys (browse mode):
H/Shift+H Heading next/prev D/Shift+D Landmark next/prev
F Next form field B Next button
K Next link T Next table
Table nav: Ctrl + Alt + Arrows
Mode switch: NVDA + Space (browse <-> focus)
JAWS Quick Keys
H Next heading T Next table F Next form field
B Next button ; Next landmark G Next graphic
Insert + F7 Link list Insert + F6 Heading list
Test Script (All Screen Readers)
- Page load - Title announced? Main landmark found? Skip link works?
- Landmark nav - All main areas reachable? Properly labeled?
- Heading nav - Logical structure? All sections discoverable?
- Form testing - Labels read? Required fields announced? Errors announced? Focus moved to error?
- Interactive elements - Each announces role + state? Activates with Enter/Space?
- Dynamic content - Updates announced? Modal traps focus? Focus returns on close?
Common Screen Reader Fixes
<!-- Button without visible text -->
<button aria-label="Close dialog"><svg aria-hidden="true">...</svg></button>
<!-- Dynamic content not announced -->
<div role="status" aria-live="polite">New results loaded</div>
<!-- Form error not read -->
<input type="email" aria-invalid="true" aria-describedby="email-error">
<span id="email-error" role="alert">Invalid email</span>
WCAG 2.2 Audit Checklist
Critical Violations (Blockers)
- All functional images have alt text; decorative images
alt="" - All interactive elements keyboard accessible (no traps)
- All form inputs have associated labels
- Color contrast: 4.5:1 text, 3:1 large text/UI components
- No auto-playing media without controls
Serious Violations
- Skip to main content link present
- Page titles unique and descriptive
- Heading hierarchy logical (no skipped levels)
- ARIA landmarks defined (main, nav, etc.)
- Focus indicator visible on all elements (3:1 contrast)
-
<html lang="en">attribute set
Forms & Interaction
- Error messages identify field and describe problem
- Required fields indicated (not color-only)
-
aria-invalid="true"+aria-describedbyon error fields - Live regions:
role="status"(polite) /role="alert"(assertive) - Modal dialogs:
role="dialog",aria-modal="true", focus trap, Esc to close
Responsive & Motion
- Content reflows at 320px (no horizontal scroll)
- Text resizes to 200% without loss
-
@media (prefers-reduced-motion: reduce)disables animations -
@media (prefers-contrast: high)increases contrast
Remediation Patterns
Keyboard nav widgets: Custom interactive widgets need: tabindex="0", role, aria-expanded, and keydown handler for Enter/Space (activate), Escape (close), Arrow keys (navigate).
Focus management for modals: Save document.activeElement before opening. Trap Tab/Shift+Tab within modal's focusable elements. Restore focus on close.
Tab interface: Use role="tablist"/role="tab"/role="tabpanel" with aria-selected, aria-controls, aria-labelledby. Arrow keys navigate tabs, Home/End jump to first/last.
Color contrast: Use @media (prefers-contrast: high) and visible :focus outlines (3px solid, 2px offset).
Cross-References
- frontend:design-system-patterns -- accessible headless components, ARIA defaults
- frontend:form-patterns -- accessible forms, labels, error messages, aria-describedby
- frontend:web-animation-patterns -- reduced-motion preferences, prefers-reduced-motion