Web Accessibility Audit and Non-Breaking Fix Pass
Run an evidence-based WCAG 2.1 AA pass over an app (or a scoped part of it), write a
dated audit record, and when asked to implement, apply only changes that are
additive and visually non-breaking, quarantining anything that would change the UI
into a design-decision list. This two-bucket split is the core of the skill: it lets
accessibility work ship without design sign-off stalling it.
Written for React and Tailwind, but the method, lenses, instruments and pitfalls port to
any component-based frontend.
Before you start
Read the project's frontend conventions and design-token documentation, and follow any
rule the repository states about which docs must be read before writing code. If a
previous accessibility audit record exists, read it first so you extend it rather than
rediscover it.
Then locate the project's existing a11y primitives, because Phase C depends on them:
| Primitive |
What to look for |
| Modal contract |
A hook or wrapper owning focus-in, Tab trap, Escape, focus restore, scroll lock |
| Skip link |
An sr-only focus:not-sr-only link targeting the main landmark |
| Route-change focus |
A component that focuses <main> on SPA navigation |
| Combobox pattern |
An existing autocomplete with correct keyboard handling, to copy |
| In-text link style |
The class combination used for links inside prose |
Any of these that does not exist is itself a finding, and building it once is cheaper
than fixing its absence per component.
Mode: report or implement?
Default to report-only when the user asks to "audit", "check", or "review":
findings plus ready-to-apply snippets, no code changes. Implement only when asked
("fix it", "apply", "implement"). When implementing, the project's own definition of
done applies: unit tests for new behavior, the repository's verify and build commands
green, and any translated string added to every locale the project ships.
Phase A: audit
Sweep the four lenses below. For a full-app audit, fan out one parallel read-only
exploration agent per lens; for a scoped audit (one page or component), check the lens
checklists inline. The per-lens checklists, grep patterns, and WCAG SC mappings are in
references/audit-lenses.md. Read it before sweeping.
- Keyboard navigation: unreachable or invisible controls, mouse-only widgets,
arrow-key patterns,
tabIndex hygiene, hover-only reveals, drag-only interactions.
- Focus management: modal contract (trap, restore, Escape, dialog ARIA), skip
link, route-change focus, live regions for dynamic updates.
- Content and readability: heading hierarchy per rendered state, text size floors,
sanitized rich text, reduced-motion coverage.
- Landmarks: one
<main>, labeled <nav>s, banner and contentinfo, content
stranded outside all landmarks.
Ground your findings in three objective instruments. Subjective sweeps alone miss things
and over-report others:
- Lint: run the project's linter and filter for the
jsx-a11y/* rules (or the
equivalent for your framework). If the codebase is clean at error severity, keep it
that way, and note what that tells you: the remaining problems are exactly the ones
static analysis cannot see, namely focus behavior, contrast, and landmark labeling.
Never downgrade an a11y rule to make a finding go away.
- Contrast math:
node scripts/contrast.mjs computes WCAG ratios from the real
token values in the Tailwind config, or --pair '#fg,#bg' for ad-hoc checks. Never
eyeball contrast, and never trust hex values written in documentation. Design docs
drift from the config, and the config is what ships.
- Automated scan in a real browser: run axe against the built app end to end. Make
sure it boots first (see the pitfall below), because a scan of an unmounted shell
reports zero violations and looks like success.
Verify load-bearing claims first-hand before they go in the report: open the file at the
cited line. Exploration agents are good at coverage and mediocre at precision.
Phase B: report
Write the report wherever the project keeps dated records, named for its own convention.
Structure:
# YYYY-MM-DD — <scope> Accessibility Audit (WCAG 2.1 AA)
Status / scope / method
## Executive summary (scorecard per lens + top issues ranked by user impact)
## Findings per lens (file:line evidence, WCAG SC number, ready-to-apply snippet)
## Tooling ratchets (zero-UI-impact CI/lint improvements)
## Quarantined: needs design sign-off (every visible change, with measured ratios)
## Appendices (contrast tables, capability matrices)
Rank by user impact, not by WCAG severity. Include what is already good, because it tells
the reader (and the next audit) which patterns to preserve. Every finding carries its
evidence as file:line and its fix as a snippet, so implementation is one approval away.
Phase C: implement the non-breaking slice only
The "truly additive" test. A change qualifies only if a sighted mouse user sees nothing
different:
sr-only markup, aria-* attributes, landmark labels: always safe.
- Same-class tag swaps (
h2 to h1, h3 to h2, nav to div): pixel-identical,
because Tailwind Preflight unstyles headings and all styling lives in the classes.
focus-visible: styles on buttons and links: keyboard-only, safe.
focus-visible: on text inputs: NOT safe. Text fields match :focus-visible on
mouse click too, so the ring shows during normal mouse use. Visible change, so
quarantine it.
- Color, text-size and underline changes: quarantine with measured before and after
ratios.
Fix with the project's primitives (the ones you located above) instead of hand-rolling.
The cookbook with adoption snippets is references/fix-patterns.md. Read it before
writing fixes.
Phase D: verify
- Run the project's full verification command (lint, types, unit tests).
- Run the end-to-end suite against a fresh build, and confirm the app actually mounted
before trusting a clean axe result.
- Gate on it: block critical and serious axe violations in CI. Exempting
color-contrast while design decisions are pending is reasonable, as long as the
exemption is recorded and temporary. Assert the skip-link and modal focus contracts
behaviorally, not just structurally.
- Add new pages and flows to the scanned list. Coverage only ratchets up.
Update the audit record with what shipped, what was corrected, and what stays
quarantined. The record is what the next audit builds on.
Pitfalls
Each of these has burned a real audit at least once.
- Lint-clean is not accessible.
role="presentation" on a clickable backdrop
silences the linter while making a dialog semantically invisible. Lint verifies
syntax; you verify behavior.
- A passing e2e suite may be scanning an empty page. If the app needs public
environment variables to boot and CI supplies none, the SPA never mounts under a
preview server and every axe scan passes vacuously against the boot skeleton. If axe
reports zero violations on a page you know has issues, confirm the app mounted.
aria-modal="true" does not trap focus. Only a keydown handler does. Counting
attributes will tell you a codebase is compliant when almost none of its modals trap.
- Do not fabricate landmark labels' language. Screen readers append the role, so
label a nav "Primary", not "Primary navigation".
- Unknown attributes may be deliberate. Custom attributes can belong to a
machine-readable layer (agent or automation metadata) and be declared in the project's
type definitions. Check before stripping them.
- Documentation drifts from config. If a documented hex or token value contradicts
the Tailwind config, the config is truth. Fix the doc in the same change.
1---2name: a11y-audit3description: Audit and fix web accessibility (WCAG 2.1 AA) in a web app, covering keyboard navigation, focus management, screen-reader semantics (landmarks, headings, ARIA), and color contrast, then implement the non-breaking fixes using shared a11y primitives instead of hand-rolled ones. Use whenever the user mentions accessibility, a11y, WCAG, screen readers, keyboard navigation, focus management, contrast, ARIA, or skip links; and also when they describe a symptom without naming accessibility ("can't use checkout with a keyboard", "focus gets lost when the modal closes", "this text is hard to read"). Applies to a single component as much as a full-app audit.4---56# Web Accessibility Audit and Non-Breaking Fix Pass78Run an evidence-based WCAG 2.1 AA pass over an app (or a scoped part of it), write a9dated audit record, and when asked to implement, apply only changes that are10**additive and visually non-breaking**, quarantining anything that would change the UI11into a design-decision list. This two-bucket split is the core of the skill: it lets12accessibility work ship without design sign-off stalling it.1314Written for React and Tailwind, but the method, lenses, instruments and pitfalls port to15any component-based frontend.1617## Before you start1819Read the project's frontend conventions and design-token documentation, and follow any20rule the repository states about which docs must be read before writing code. If a21previous accessibility audit record exists, read it first so you extend it rather than22rediscover it.2324Then locate the project's existing a11y primitives, because Phase C depends on them:2526| Primitive | What to look for |27|---|---|28| Modal contract | A hook or wrapper owning focus-in, Tab trap, Escape, focus restore, scroll lock |29| Skip link | An `sr-only focus:not-sr-only` link targeting the main landmark |30| Route-change focus | A component that focuses `<main>` on SPA navigation |31| Combobox pattern | An existing autocomplete with correct keyboard handling, to copy |32| In-text link style | The class combination used for links inside prose |3334Any of these that does not exist is itself a finding, and building it once is cheaper35than fixing its absence per component.3637## Mode: report or implement?3839Default to **report-only** when the user asks to "audit", "check", or "review":40findings plus ready-to-apply snippets, no code changes. Implement only when asked41("fix it", "apply", "implement"). When implementing, the project's own definition of42done applies: unit tests for new behavior, the repository's verify and build commands43green, and any translated string added to every locale the project ships.4445## Phase A: audit4647Sweep the four lenses below. For a full-app audit, fan out one parallel read-only48exploration agent per lens; for a scoped audit (one page or component), check the lens49checklists inline. The per-lens checklists, grep patterns, and WCAG SC mappings are in50`references/audit-lenses.md`. Read it before sweeping.51521. **Keyboard navigation**: unreachable or invisible controls, mouse-only widgets,53 arrow-key patterns, `tabIndex` hygiene, hover-only reveals, drag-only interactions.542. **Focus management**: modal contract (trap, restore, Escape, dialog ARIA), skip55 link, route-change focus, live regions for dynamic updates.563. **Content and readability**: heading hierarchy per rendered state, text size floors,57 sanitized rich text, reduced-motion coverage.584. **Landmarks**: one `<main>`, labeled `<nav>`s, banner and contentinfo, content59 stranded outside all landmarks.6061Ground your findings in three objective instruments. Subjective sweeps alone miss things62and over-report others:6364- **Lint**: run the project's linter and filter for the `jsx-a11y/*` rules (or the65 equivalent for your framework). If the codebase is clean at `error` severity, keep it66 that way, and note what that tells you: the *remaining* problems are exactly the ones67 static analysis cannot see, namely focus behavior, contrast, and landmark labeling.68 Never downgrade an a11y rule to make a finding go away.69- **Contrast math**: `node scripts/contrast.mjs` computes WCAG ratios from the real70 token values in the Tailwind config, or `--pair '#fg,#bg'` for ad-hoc checks. Never71 eyeball contrast, and never trust hex values written in documentation. Design docs72 drift from the config, and the config is what ships.73- **Automated scan in a real browser**: run axe against the built app end to end. Make74 sure it boots first (see the pitfall below), because a scan of an unmounted shell75 reports zero violations and looks like success.7677Verify load-bearing claims first-hand before they go in the report: open the file at the78cited line. Exploration agents are good at coverage and mediocre at precision.7980## Phase B: report8182Write the report wherever the project keeps dated records, named for its own convention.83Structure:8485```86# YYYY-MM-DD — <scope> Accessibility Audit (WCAG 2.1 AA)87Status / scope / method88## Executive summary (scorecard per lens + top issues ranked by user impact)89## Findings per lens (file:line evidence, WCAG SC number, ready-to-apply snippet)90## Tooling ratchets (zero-UI-impact CI/lint improvements)91## Quarantined: needs design sign-off (every visible change, with measured ratios)92## Appendices (contrast tables, capability matrices)93```9495Rank by user impact, not by WCAG severity. Include what is already good, because it tells96the reader (and the next audit) which patterns to preserve. Every finding carries its97evidence as `file:line` and its fix as a snippet, so implementation is one approval away.9899## Phase C: implement the non-breaking slice only100101The "truly additive" test. A change qualifies only if a sighted mouse user sees nothing102different:103104- `sr-only` markup, `aria-*` attributes, landmark labels: always safe.105- Same-class tag swaps (`h2` to `h1`, `h3` to `h2`, `nav` to `div`): pixel-identical,106 because Tailwind Preflight unstyles headings and all styling lives in the classes.107- `focus-visible:` styles on **buttons and links**: keyboard-only, safe.108- `focus-visible:` on **text inputs**: NOT safe. Text fields match `:focus-visible` on109 mouse click too, so the ring shows during normal mouse use. Visible change, so110 quarantine it.111- Color, text-size and underline changes: quarantine with measured before and after112 ratios.113114Fix with the project's primitives (the ones you located above) instead of hand-rolling.115The cookbook with adoption snippets is `references/fix-patterns.md`. Read it before116writing fixes.117118## Phase D: verify1191201. Run the project's full verification command (lint, types, unit tests).1212. Run the end-to-end suite against a fresh build, and confirm the app actually mounted122 before trusting a clean axe result.1233. Gate on it: block critical and serious axe violations in CI. Exempting124 `color-contrast` while design decisions are pending is reasonable, as long as the125 exemption is recorded and temporary. Assert the skip-link and modal focus contracts126 behaviorally, not just structurally.1274. Add new pages and flows to the scanned list. Coverage only ratchets up.128129Update the audit record with what shipped, what was corrected, and what stays130quarantined. The record is what the next audit builds on.131132## Pitfalls133134Each of these has burned a real audit at least once.135136- **Lint-clean is not accessible.** `role="presentation"` on a clickable backdrop137 silences the linter while making a dialog semantically invisible. Lint verifies138 syntax; you verify behavior.139- **A passing e2e suite may be scanning an empty page.** If the app needs public140 environment variables to boot and CI supplies none, the SPA never mounts under a141 preview server and every axe scan passes vacuously against the boot skeleton. If axe142 reports zero violations on a page you know has issues, confirm the app mounted.143- **`aria-modal="true"` does not trap focus.** Only a keydown handler does. Counting144 attributes will tell you a codebase is compliant when almost none of its modals trap.145- **Do not fabricate landmark labels' language.** Screen readers append the role, so146 label a nav "Primary", not "Primary navigation".147- **Unknown attributes may be deliberate.** Custom attributes can belong to a148 machine-readable layer (agent or automation metadata) and be declared in the project's149 type definitions. Check before stripping them.150- **Documentation drifts from config.** If a documented hex or token value contradicts151 the Tailwind config, the config is truth. Fix the doc in the same change.