plugins/frontend-toolkit/skills/accessibility-audit/SKILL.md
WCAG 2.2 AA audit — semantic HTML, axe-core + eslint-plugin-jsx-a11y, keyboard navigation, ARIA, color contrast, target size, focus-not-obscured, prefers-reduced-motion. Use when completing a new component, after design system changes, or before shipping. Not for form label/aria-describedby patterns (use form-ux) or baking a11y into shared components from the start (use design-system-construction).
npx skillsauth add jaykim88/claude-ai-engineering accessibility-auditInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Meet WCAG 2.2 AA (the current W3C standard — 2.2 adds target size, dragging alternatives, and focus-not-obscured on top of 2.1). Automated tools catch ~57% of issues (Deque 2021 study) — combine static lint + axe-core with manual keyboard testing and screen reader sanity checks.
Universal — WCAG 2.2 AA, axe-core, semantic HTML rules, and keyboard navigation requirements apply to every web framework. Only the syntax for ARIA attributes and motion-reduce variants differs by template language.
Semantic HTML — replace generic with specific
<div onClick> → <button> (focusable, keyboard-accessible, screen-reader-recognized by default)<a href> not <button onClick={() => router.push()}><main>, <nav>, <header>, <footer>, <aside> (instead of generic <div>)<html lang> so screen readers use the correct pronunciation (WCAG 3.1.1)Audit images
<img> has alt attributealt="" (explicit empty, screen reader skips)altnext/image enforces alt — keep it lint-enforcedVerify heading hierarchy
h1 → h2 → h3 (no skipping levels)h1 per pageColor contrast checks
Keyboard navigation — manual walkthrough
outline: none without replacement)Focus management for dynamic UI
h1 or use aria-live)aria-busy or aria-live="polite"ARIA where semantic HTML doesn't suffice
role, aria-label, aria-describedby, aria-expanded, aria-controlsaria-label on an icon <button> with no visible text (the inverse of the anti-pattern — text buttons need none)aria-live="polite" for non-urgent updates, assertive for criticalautocomplete, WCAG 1.3.5) and errors are programmatically identified (aria-invalid + message, WCAG 3.3.1) — build pattern lives in form-uxgrep -rn 'aria-' src/ and verify correctnessReduced motion
prefers-reduced-motion respectmotion-reduce: variantanimation-quality skill8b. Pointer, zoom & WCAG 2.2 checks
responsive-design touch targets (44/48px is stricter)Automated audit (validation loop)
eslint-plugin-jsx-a11y catches missing alt, invalid ARIA, and handlers on non-interactive elements at dev time — cheaper than runtime axetags: ['wcag2a','wcag2aa','wcag21aa','wcag22aa'])cicd-pipeline skill for wiring lint + axe into the GitHub Actions matrix as a blocking step.Manual screen reader check
Interactive element semantics
// ❌ Not focusable, not keyboard-accessible, screen readers don't announce it as interactive
<div onClick={handleSave} className="cursor-pointer">Save</div>
// ✅ Focusable by default, Enter/Space activates, announced as "button"
<button onClick={handleSave} type="button">Save</button>
Focus indicator removal
/* ❌ Strips focus indicator without replacement — keyboard users lost */
*:focus { outline: none; }
/* ✅ Custom focus ring with sufficient contrast */
*:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
| ❌ Anti-pattern | ✅ Correct |
|---|---|
| <div onClick={handler}> | <button onClick={handler}> (focusable, keyboard-accessible by default) |
| outline: none without replacement | Custom focus ring with sufficient contrast (focus-visible:ring-2) |
| tabindex="0" on everything | Only interactive elements need focus order |
| aria-label on a <button>Submit</button> | Button text is the label; no aria-label needed |
| Icon-only <button> with no text or aria-label | <button aria-label="Close">✕</button> |
| Error shown only by red color | Color + icon/text (don't rely on color alone, 1.4.1) |
| h1 → h3 skipping h2 | Sequential heading levels (h1 → h2 → h3) |
| <img src="logo.png"> with no alt | <img alt="Company Name logo"> or alt="" for decorative |
| Tier | Examples | Action SLA |
|---|---|---|
| Critical | Keyboard-trap (modal can't be closed via keyboard); image with no alt blocking core flow; color contrast < 3:1 for body text; icon-only control with no accessible name on a core action | Block release; fix immediately |
| Major | Missing focus indicator on interactive elements; heading-skip violations; missing aria-describedby on form errors; meaning conveyed by color alone (1.4.1); no skip link; focused element obscured by sticky header (2.4.11) | Fix this sprint |
| Minor | Decorative images missing explicit alt=""; pointer target < 24px (2.5.8); missing <html lang>; non-critical aria-label improvements | Schedule within 2 sprints |
eslint-plugin-jsx-a11y cleanoutline: none without replacement focus indicator; focus not obscured by sticky UI (2.4.11)<html lang> setprefers-reduced-motion honored<div onClick> → <button> everywhere) — verify each replacement preserves layout and event semanticsdocs/a11y-audit-YYYY-MM-DD.md with sections:
## Summary — axe-core violations / Lighthouse score / manual findings## Critical findings — per finding: file:line, WCAG criterion (e.g., 1.4.3 Contrast), screen reader impact, fix## Major findings — same format## Minor findings — same format## Manual testing notes — keyboard nav, screen reader walkthrough resultsfix(a11y): <description> [WCAG-N.N.N]cicd-pipeline skill)<button>, <nav>, <main>, <article> (no <div onClick>)aria-label, aria-describedby, aria-live, aria-expanded as JSX propsmotion-reduce: variant or CSS @media (prefers-reduced-motion: reduce)eslint-plugin-jsx-a11y (ships in eslint-config-next) — enable the strict rulesetfocus-visible:ring-2 ring-offset-2 (visible, sufficient size/contrast)@axe-core/react (dev) + axe-core/playwright (CI)aria-label="..."); reduced motion via Tailwind or CSS @media<div on:click> → warning)[attr.aria-label]="..."); Angular CDK provides FocusTrap, LiveAnnouncerdesign-system-construction — a11y must be baked into shared components, not retrofittedanimation-quality — prefers-reduced-motion respect is enforced jointlyform-ux — label/aria-describedby, autocomplete (1.3.5), error identification (3.3.1) build patternsresponsive-design — touch-target size and reflow overlap with WCAG 2.5.8 / 1.4.10cicd-pipeline — wire eslint-plugin-jsx-a11y + axe-core into the GitHub Actions matrixeslint-plugin-jsx-a11y), then gate violations in CI with axe tags: ['wcag2a','wcag2aa','wcag21aa','wcag22aa']. Automation catches only ~57% of issues — always pair with manual keyboard traversal and at least one screen reader sanity check before shipping. Semantic HTML is the cheapest accessibility tool: <button> is better than <div onClick> + 5 ARIA attributes. Target WCAG 2.2 AA (current standard): beyond 2.1, that means ≥24px target size (2.5.8), focus not obscured (2.4.11), and a non-drag alternative (2.5.7).development
Audit and optimize third-party scripts — analytics, tag managers, chat widgets, embeds — with the right loading strategy, performance budget, facades, and CSP/consent controls. Use when adding a script, when TBT/INP regress, when a GDPR/CCPA consent requirement arises, or before shipping. Not for first-party bundle size (use bundle-optimization) or broad Core Web Vitals diagnosis (use rendering-performance).
development
Apply the Testing Trophy (mostly integration tests with RTL + MSW, sparing E2E with Playwright) and set coverage thresholds. Use before new feature work, after bug fixes, when CI coverage falls below target, or when tests are flaky or break on every refactor. Not for wiring coverage gates + Playwright into the GitHub Actions matrix (use cicd-pipeline) or auditing WCAG a11y compliance (use accessibility-audit).
development
Inventory and prioritize technical debt — TODO/FIXME/HACK, any usage, deprecated APIs, untested logic — with impact × effort matrix. Use at quarter start, before a refactoring sprint, when a new teammate joins, or when feature velocity slows. Not for actually paying down debt (use code-refactoring) or recording a migration approach (use decision-records) — this only inventories and prioritizes.
development
Decision framework for choosing the right state location — URL, server cache, local component, or shared/global store. Use when state-sync bugs appear, prop drilling gets deep (3+ levels), filters/tabs lose state on reload, or quarterly review. Not for form state specifically (use form-ux) or when the state is actually server data (use api-caching-optimization).