--- name: accessibility description: "Use when making a web UI conform to WCAG 2.2 Level AA — axe-core or Lighthouse a11y violations, keyboard operability, focus management, ARIA roles/names/live regions, contrast, tap-target size. NOT palette or visual intent (that is `design`), NOT test-runner setup (that is `testing-web`), NOT LCP/page-speed (that is `performance`)." tags: [wcag, accessibility, a11y, aria, axe-core] recommends: [testing-web, e2e-testing, design, react, performance] origin: risco --- # Accessibility — Ship WCAG 2.2 AA, not vibes *The bar is conformance to WCAG 2.2 Level AA. "Looks fine to me" is not a measurement. Fix the semantics, scan what a machine can scan, then walk the part it can't.* ## The loop (your 30-second model) Run these in order. Skipping a step front-loads rework. 1. **Native semantics first.** A real ` ``` | You want… | Use native… | Not… | | ---------------------- | -------------------------- | ----------------------------- | | A click action | ` ``` ## Automate it (versioned, 2026-06-02) Three layers — each catches what the cheaper one can't. **Lint (static, JSX only) — `eslint-plugin-jsx-a11y` 6.10.2.** Catches missing `alt`, label-less inputs, positive `tabindex`, invalid roles, at edit time. ```jsonc // .eslintrc — extends, then runs in your existing lint step { "extends": ["plugin:jsx-a11y/recommended"] } ``` **Unit (fast, no browser) — `jest-axe` 10.0.0.** Asserts no axe violations on rendered output. Remember: **contrast is off in jsdom.** ```js import { axe, toHaveNoViolations } from "jest-axe"; expect.extend(toHaveNoViolations); test("no a11y violations", async () => { const { container } = render(); expect(await axe(container)).toHaveNoViolations(); }); ``` **Browser (the real thing, catches contrast) — `@axe-core/playwright` 4.11.3** (on `axe-core` 4.12.0). Scope it to the WCAG 2.2 AA tags: ```js import AxeBuilder from "@axe-core/playwright"; const results = await new AxeBuilder({ page }) .withTags(["wcag2a", "wcag2aa", "wcag22aa"]) .analyze(); expect(results.violations).toEqual([]); ``` **Lighthouse a11y score** is a smoke signal for a quick pulse, not proof — it runs a subset of axe and gives a number, not a pass. `scripts/verify.sh` ties this together: it detects whatever tooling the project has and runs it, failing only on serious/critical violations (read-only, skips cleanly when no tooling is present). ## Manual checklist (the ~43% a machine can't see) Do these by hand before you call it done: - [ ] **Unplug the mouse.** Tab through the entire flow — every control reachable, order logical, focus always visible, no trap, Escape closes overlays. - [ ] **One screen-reader spot check** — VoiceOver (macOS, ⌘F5) or NVDA (Windows). Do names, roles, and state read sensibly? Are errors announced? - [ ] **Zoom to 200%** — no content lost, no horizontal scroll, nothing clipped. - [ ] **`prefers-reduced-motion`** honored — no autoplay parallax/animation that ignores it. - [ ] **Alt text is meaningful, not decorative-as-content** — informative images describe; decorative images use `alt=""`. Full AA checklist grouped by POUR, with the per-item auto/manual split and the 6 new 2.2 criteria flagged → `references/wcag22-checklist.md`. ## Anti-patterns | Anti-pattern | Why it fails | Do instead | | ---------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------- | | `
` | No keyboard, no focus, you owe all behavior by hand | `