| name | accessibility-testing |
| description | Automated accessibility testing and WCAG compliance validation. Use when: adding accessibility tests to a project, configuring axe-core or pa11y, integrating a11y checks into CI, auditing an existing application for accessibility violations, validating ARIA usage, testing keyboard navigation, or building an accessibility regression suite. Covers axe-core, Playwright a11y testing, Lighthouse accessibility audits, CI integration, and WCAG 2.2 AA compliance. |
| tags | ["developer","designer"] |
Accessibility Testing
When to Use
- Adding automated accessibility tests to a web project
- Configuring axe-core, pa11y, or Lighthouse for a11y audits
- Integrating accessibility checks into CI/CD pipelines
- Auditing an existing application for WCAG 2.2 AA violations
- Validating ARIA attributes and roles
- Testing keyboard navigation flows
- Building an accessibility regression suite
- Retrofitting a11y testing into a project that has none
Core Principle: Automate the Automatable, Manually Test the Rest
Automated tools catch ~30-40% of WCAG violations (missing alt text, low contrast, invalid ARIA). The rest requires manual testing with screen readers and keyboard navigation. Automate the detectable, then build manual test plans for the rest.
Testing Pyramid for Accessibility
| Layer | Tool | Catches | Speed |
|---|
| Static analysis | eslint-plugin-jsx-a11y | Missing alt, invalid ARIA in JSX | Instant (lint) |
| Component tests | @axe-core/react, vitest-axe | Per-component violations | Fast (unit) |
| Integration tests | Playwright + axe-core | Full-page violations, focus flow | Medium |
| Audit | Lighthouse CI | WCAG score, best practices | Slow (CI) |
| Manual | Screen reader + keyboard | Context, flow, comprehension | Slowest |
Rule: Start from the top. Catch what you can at lint time before it reaches tests.
Static Analysis (ESLint)
Setup
npm install -D eslint-plugin-jsx-a11y
Config (eslint.config.ts)
import jsxA11y from "eslint-plugin-jsx-a11y";
export default [
jsxA11y.flatConfigs.recommended,
];
What It Catches
- Missing
alt on <img>
- Invalid ARIA attributes and roles
- Missing
htmlFor on <label>
- Non-interactive elements with click handlers (missing
role and keyboard support)
- Missing
lang on <html>
Component-Level Testing (Vitest + axe-core)
Setup
npm install -D @axe-core/react vitest-axe jsdom
Test Pattern
import { render } from "@testing-library/react";
import { axe, toHaveNoViolations } from "vitest-axe";
import { expect, test } from "vitest";
import { MyComponent } from "./MyComponent";
expect.extend(toHaveNoViolations);
test("MyComponent has no accessibility violations", async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Rules
- Add an
axe test for every major component (forms, modals, navigation, tables).
- Run axe tests as part of the standard test suite — they execute in milliseconds.
- When a violation is found, fix the component, not the test. Never disable axe rules without documenting why.
Integration Testing (Playwright + axe-core)
Setup
npm install -D @playwright/test @axe-core/playwright
Full-Page Audit
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
test("homepage has no accessibility violations", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag22aa"])
.analyze();
expect(results.violations).toEqual([]);
});
Scoped Audit (Specific Section)
test("checkout form is accessible", async ({ page }) => {
await page.goto("/checkout");
const results = await new AxeBuilder({ page })
.include("#checkout-form")
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(results.violations).toEqual([]);
});
Keyboard Navigation Test
test("modal can be operated with keyboard only", async ({ page }) => {
await page.goto("/");
await page.click('[data-testid="open-modal"]');
const modal = page.locator('[role="dialog"]');
await expect(modal).toBeFocused();
await page.keyboard.press("Tab");
await expect(page.locator('[data-testid="modal-input"]')).toBeFocused();
await page.keyboard.press("Escape");
await expect(modal).not.toBeVisible();
await expect(page.locator('[data-testid="open-modal"]')).toBeFocused();
});
Rules
- Test every route/page for WCAG 2.2 AA compliance.
- Test interactive flows (forms, modals, dropdowns) for keyboard operability.
- Test focus management: opening modals traps focus, closing returns it to trigger.
- Use
withTags(["wcag2a", "wcag2aa", "wcag22aa"]) to target the correct WCAG level.
Lighthouse CI
Setup
npm install -D @lhci/cli
Config (lighthouserc.js)
module.exports = {
ci: {
collect: {
url: ["http://localhost:3000/", "http://localhost:3000/dashboard"],
startServerCommand: "npm run preview",
numberOfRuns: 3,
},
assert: {
assertions: {
"categories:accessibility": ["error", { minScore: 0.9 }],
"color-contrast": "error",
"image-alt": "error",
"label": "error",
"link-name": "error",
"button-name": "error",
},
},
upload: {
target: "temporary-public-storage",
},
},
};
CI Integration (Drone)
- name: lighthouse-a11y
image: node:22-alpine
commands:
- npm ci
- npm run build
- npx @lhci/cli autorun
Rules
- Accessibility score must be ≥90 (0.9). Regression below this blocks the build.
- Specific checks (
color-contrast, image-alt, label, link-name, button-name) are always errors.
- Run Lighthouse 3 times and take the median score to reduce flakiness.
WCAG 2.2 AA Compliance Checklist
The most commonly violated WCAG criteria:
Perceivable
Operable
Understandable
Robust
Screen Reader Testing (Manual)
Tools
| OS | Screen Reader | Browser |
|---|
| macOS | VoiceOver (built-in) | Safari |
| Windows | NVDA (free) | Firefox or Chrome |
| Windows | JAWS (paid) | Chrome |
Test Script
- Navigate to the page using only keyboard (Tab, Enter, Arrow keys, Escape).
- Listen to how the screen reader announces page landmarks, headings, and interactive elements.
- Complete the primary user flow (e.g., submit a form, navigate to a detail page) without using the mouse.
- Verify that dynamic content changes (modals, toasts, live regions) are announced.
- Check that images are described meaningfully and decorative images are skipped.
Handling Violations
Severity Triage
| axe-core Impact | Action |
|---|
| critical | Fix before merge. Blocks users entirely. |
| serious | Fix before merge. Significant barrier. |
| moderate | Fix within current sprint. Degraded experience. |
| minor | Add to backlog. Low-impact inconvenience. |
Disabling Rules
When a rule must be disabled (rare), document it:
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.disableRules(["color-contrast"])
.analyze();
Rule: Never disable a rule without a tracking issue and a comment explaining why.
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|
aria-label on everything | Overrides visible text, confuses screen readers | Use visible labels; ARIA is a last resort |
outline: none without replacement | Keyboard users can't see focus | Use :focus-visible with custom styles |
role="button" on <div> | Missing keyboard support, missing semantics | Use <button> element instead |
| Testing only with automation | Misses flow, context, and comprehension issues | Pair automation with manual screen reader testing |
| Fixing tests instead of components | Violations persist for users | Fix the component, then verify the test passes |
tabindex="1" or higher | Breaks natural focus order | Only use tabindex="0" or tabindex="-1" |
Audit Checklist
When auditing an existing project for accessibility testing: