Skip to content

Testing for Accessibility

Accessibility testing ensures your application is usable by people with disabilities. It requires a combination of automated scanning (which catches about 30-40 percent of issues) and manual testing (which catches the rest). Neither approach alone is sufficient.


The Testing Pyramid for Accessibility

┌──────────┐
│ User │ Testing with real users
│ Testing │ who have disabilities
┌┴──────────┴┐
│ Manual │ Keyboard navigation,
│ Testing │ screen reader testing
┌┴────────────┴┐
│ Integration │ Component tests with
│ Tests │ a11y assertions
┌┴──────────────┴┐
│ Automated │ axe-core, Lighthouse,
│ Scanning │ Pa11y in CI/CD
└────────────────┘
Coverage: Automated ~30-40% | Manual ~60-70% | Users ~90%+

Manual Testing: Keyboard Navigation

Keyboard testing is the most important manual test. If your application is not keyboard-accessible, it is not accessible.

Keyboard Testing Checklist

ActionKeysWhat to Check
Navigate forwardTabFocus moves to next interactive element
Navigate backwardShift + TabFocus moves to previous element
Activate button/linkEnterButtons and links activate
Toggle checkboxSpaceCheckbox state changes
Select radioArrow keysMove between radio options
Open dropdownEnter / Space / Arrow DownDropdown opens
Navigate dropdownArrow Up/DownMove between options
Close dialog/menuEscapeDialog closes, focus returns
ScrollSpace / Page Up / Page DownPage scrolls
Navigate tabsArrow Left/RightMove between tab buttons

What to Look For

✓ Every interactive element is reachable by Tab
✓ Focus order is logical (left to right, top to bottom)
✓ Focus is visible (clear focus indicator on every element)
✓ No keyboard traps (focus can always move away)
✓ Skip navigation link works (bypass repetitive content)
✓ Modals trap focus inside (Tab cycles within dialog)
✓ After closing a modal, focus returns to the trigger
✓ Custom widgets have correct keyboard behavior
✓ Dropdown menus are navigable with arrow keys
✓ Form validation errors are announced

Common Keyboard Issues

IssueCauseFix
Element not focusableUsing <div> instead of <button>Use semantic elements or add tabindex="0"
No visible focusCSS outline: none without replacementProvide custom focus styles
Focus trapModal does not allow Tab to escapeImplement focus trapping with Escape key exit
Focus lostElement removed from DOM while focusedMove focus to a logical destination
Skip link missingNo bypass mechanismAdd skip link as first element in body
Wrong tab orderMisuse of tabindex values greater than 0Use tabindex="0" and rely on DOM order

Manual Testing: Screen Readers

Screen reader testing is essential for verifying that your application makes sense when content is read aloud.

Screen Reader Basics

Getting Started:
Enable: Cmd + F5 (or System Settings → Accessibility → VoiceOver)
Toggle: Cmd + F5 to turn off
Essential Commands:
VO key = Control + Option
Navigate next: VO + Right Arrow
Navigate previous: VO + Left Arrow
Activate: VO + Space
Read all: VO + A
Read heading: VO + Cmd + H (next heading)
Headings list: VO + U (rotor) → Headings
Landmarks list: VO + U (rotor) → Landmarks
Links list: VO + U (rotor) → Links
Web rotor: VO + U (lists headings, links, landmarks, etc.)
Rotor Navigation:
VO + U opens the rotor
Left/Right arrows switch between categories
Up/Down arrows navigate within a category

Screen Reader Testing Checklist

Content:
✓ All images have meaningful alt text (or alt="" for decorative)
✓ Headings convey document structure
✓ Links have descriptive text (not "click here")
✓ Tables have headers (th) and captions
✓ Form inputs have associated labels
✓ Error messages are announced
Navigation:
✓ Landmarks are present and labeled
✓ Heading hierarchy is logical (h1 → h2 → h3)
✓ Skip link navigates to main content
✓ Page title is descriptive
Dynamic Content:
✓ Live regions announce updates (aria-live)
✓ Modal dialogs announce their title on open
✓ Loading states are communicated
✓ Form validation errors are announced
✓ Toast/notification messages are announced
✓ Expanding/collapsing sections announce state changes
Interactive Components:
✓ Custom widgets announce their role and state
✓ Tabs announce "selected" state
✓ Checkboxes announce "checked/unchecked"
✓ Menus announce when open/closed
✓ Progress bars announce progress

Automated Testing Tools

axe-core

axe-core is the most widely used accessibility testing engine. It powers many other tools and can be integrated into unit tests, browser extensions, and CI/CD.

// jest-axe: accessibility assertions in unit tests
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('navigation has no accessibility violations', async () => {
const { container } = render(<Navigation />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('form has proper labels', async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
// Check specific rules
const labelViolations = results.violations
.filter(v => v.id === 'label');
expect(labelViolations).toHaveLength(0);
});
// With specific rules
test('color contrast meets AA standards', async () => {
const { container } = render(<Dashboard />);
const results = await axe(container, {
rules: {
'color-contrast': { enabled: true },
},
});
expect(results).toHaveNoViolations();
});

Lighthouse

Google Lighthouse includes an accessibility audit as part of its overall performance analysis:

Terminal window
# CLI usage
npx lighthouse https://example.com \
--only-categories=accessibility \
--output=json \
--output-path=./a11y-report.json
# Programmatic usage
npx lighthouse https://example.com \
--only-categories=accessibility \
--output=html \
--output-path=./a11y-report.html
# With Chrome flags
npx lighthouse https://example.com \
--only-categories=accessibility \
--chrome-flags="--headless --no-sandbox"

Pa11y

Pa11y is a command-line tool for running accessibility tests:

Terminal window
# Basic usage
npx pa11y https://example.com
# With specific standard
npx pa11y --standard WCAG2AA https://example.com
# Test multiple URLs from a config file
npx pa11y-ci
# pa11y-ci configuration (.pa11yci.json)
{
"defaults": {
"standard": "WCAG2AA",
"timeout": 30000,
"wait": 1000,
"ignore": [
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
]
},
"urls": [
"https://example.com/",
"https://example.com/login",
"https://example.com/dashboard",
{
"url": "https://example.com/settings",
"actions": [
"click element #tab-profile",
"wait for element #profile-form to be visible"
]
}
]
}

Color Contrast Checking

WCAG requires minimum contrast ratios between text and its background:

Text TypeLevel AALevel AAA
Normal text (under 18pt / 14pt bold)4.5:17:1
Large text (18pt+ or 14pt+ bold)3:14.5:1
UI components and graphics3:1Not defined

Tools for Contrast Checking

ToolTypeUse Case
axe browser extensionBrowser extensionCheck contrast on any page
Contrast (macOS app)Native appQuick checking with eyedropper
WebAIM Contrast CheckerWeb toolManual contrast ratio calculation
Stark (Figma/Sketch plugin)Design toolCheck contrast during design
Chrome DevToolsBuilt-inContrast ratio shown in color picker

Programmatic Contrast Checking

// Calculating contrast ratio
function getLuminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map(c => {
c = c / 255;
return c <= 0.03928
? c / 12.92
: Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
function getContrastRatio(color1, color2) {
const l1 = getLuminance(...color1);
const l2 = getLuminance(...color2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// Example: white text on blue background
const white = [255, 255, 255];
const blue = [37, 99, 235];
const ratio = getContrastRatio(white, blue);
console.log(`Contrast ratio: ${ratio.toFixed(2)}:1`);
// Check against WCAG thresholds
console.log(`AA (normal text): ${ratio >= 4.5 ? 'PASS' : 'FAIL'}`);
console.log(`AA (large text): ${ratio >= 3 ? 'PASS' : 'FAIL'}`);
console.log(`AAA (normal text): ${ratio >= 7 ? 'PASS' : 'FAIL'}`);

Color Blindness Considerations

Do not rely on color alone to convey information:

/* BAD: only color distinguishes valid from invalid */
.valid { color: green; }
.invalid { color: red; }
/* GOOD: color + icon + text */
.valid {
color: green;
}
.valid::before {
content: "";
}
.invalid {
color: red;
}
.invalid::before {
content: "";
}

Testing with Real Users

The most valuable accessibility testing involves people who actually use assistive technologies daily.

How to Conduct User Testing

  1. Recruit participants — Include users with different disabilities (blind, low vision, motor, cognitive)
  2. Define tasks — “Find and purchase a product,” “Create an account,” “Find the help section”
  3. Observe without helping — Note where users struggle, not just where they succeed
  4. Ask open questions — “What did you expect to happen?” “Was anything confusing?”
  5. Document findings — Record sessions (with consent), categorize issues by severity
  6. Prioritize fixes — Address blocking issues first, then usability improvements

Severity Levels

LevelDescriptionExample
CriticalCannot complete core taskForm cannot be submitted with keyboard
MajorSignificant difficulty completing taskError messages not announced to screen reader
MinorInconvenient but workaround existsFocus order is slightly unintuitive
CosmeticMinor annoyance, low impactFocus ring color could be more visible

CI/CD Integration

Integrating accessibility checks into your CI/CD pipeline catches regressions before they reach production.

.github/workflows/a11y.yml
name: Accessibility Tests
on:
pull_request:
branches: [main]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- name: Start server
run: npm run preview &
env:
PORT: 3000
- name: Wait for server
run: npx wait-on http://localhost:3000
- name: Run Pa11y CI
run: npx pa11y-ci
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v11
with:
urls: |
http://localhost:3000/
http://localhost:3000/login
http://localhost:3000/dashboard
budgetPath: ./.lighthouserc-budget.json
uploadArtifacts: true

Accessibility Testing Strategy

PhaseTesting TypeToolsFrequency
DevelopmentLinting (eslint-plugin-jsx-a11y)ESLintEvery save
Code ReviewManual review checklistChecklist + browser extensionEvery PR
Unit TestsComponent a11y assertionsjest-axeEvery commit
IntegrationPage-level scanningPlaywright + axeEvery build
CI/CDRegression preventionPa11y-CI, Lighthouse CIEvery merge
StagingManual keyboard + screen readerVoiceOver, NVDABefore release
QuarterlyUser testing with disabled usersParticipant recruitmentQuarterly