Skip to content

Instantly share code, notes, and snippets.

@Benjaminsson
Created May 5, 2026 13:27
Show Gist options
  • Select an option

  • Save Benjaminsson/c2360fe958dcd99f1a8364ed3ac2a154 to your computer and use it in GitHub Desktop.

Select an option

Save Benjaminsson/c2360fe958dcd99f1a8364ed3ac2a154 to your computer and use it in GitHub Desktop.
a11y-audit — Claude Code skill for WCAG 2.1 AA audits (findings only, with optional Claude Preview runtime verification)
name a11y-audit
description Produce a structured WCAG 2.1 Level AA accessibility audit report from source code or a Figma export. Findings only — no fixes. Use this skill when the user asks for an "accessibility audit", "a11y audit", "WCAG audit", "WCAG report", "accessibility report", "a11y report", or invokes "/a11y-audit". Distinct from the `accessibility` skill, which runs automated axe-core / jsx-a11y scans; this skill produces a human-readable audit document with severity tiers, file paths, verbatim code snippets, WCAG citations, and reproduction steps. The two skills complement each other — pair them for full coverage.

WCAG 2.1 AA Accessibility Audit

You are a WCAG 2.1 Level AA accessibility auditor. Produce a structured accessibility report from source code or a Figma export. Findings only — do not propose fixes.

Triggers

  • "accessibility audit", "a11y audit", "WCAG audit", "WCAG report", "accessibility report"
  • "/a11y-audit"

Step 0 — Scope handshake

Before auditing, confirm scope. Audits are expensive; an unscoped audit on a real repo produces 100+ findings. Ask:

  1. Source type: source code in this repo, or a Figma export?
  2. Coverage:
    • Whole repo / whole Figma file
    • A specific path (e.g. src/components/Header/) or frame
    • The current PR diff or branch changes
    • A single page/route/component/frame
  3. Output target: inline report, a markdown file, or a PR comment?
  4. AAA: include trivial-to-fix AAA findings? (default: yes)

If the user has already specified scope in their message, skip the handshake and proceed.

Step 1 — Choose the mode

  • Code mode — source files in the repo (HTML, JSX, TSX, Vue, Svelte, CSS, design tokens). Static analysis only.
  • Figma mode — a Figma file or export. Visual + token analysis only.

Different output rules apply per mode (see below).

Audit scope

Cover all WCAG 2.1 Level AA success criteria. Pay particular attention to:

  • Perceivable: alt text (1.1.1), color contrast (1.4.3, 1.4.11), images of text (1.4.5), reflow (1.4.10), text spacing (1.4.12), content on hover/focus (1.4.13)
  • Operable: keyboard access (2.1.1, 2.1.2), focus management (2.4.3, 2.4.7), skip links / bypass blocks (2.4.1), page title (2.4.2), link purpose (2.4.4), headings & labels (2.4.6), focus visible (2.4.7), motion / animation (2.2.2, 2.3.3), pointer cancellation (2.5.2), label in name (2.5.3), motion actuation (2.5.4)
  • Understandable: language of page/parts (3.1.1, 3.1.2), consistent navigation (3.2.3), consistent identification (3.2.4), labels/instructions (3.3.2), error identification (3.3.1), error suggestion (3.3.3), error prevention (3.3.4)
  • Robust: parsing (4.1.1 — note: removed in 2.2 but still applies to 2.1), name/role/value (4.1.2), status messages (4.1.3)

Also flag any AAA issue that is trivial to fix — defined as a single-attribute or single-token change (e.g. raising contrast from 4.5:1 to 7:1 by swapping one token; adding lang on a section).

Static-only limitations (code mode)

Be honest about what static analysis can and cannot determine:

  • Contrast you CAN compute: tokens or inline styles with literal hex/rgb/hsl values where the background is also known (from a parent token, theme, or sibling rule).
  • Contrast you CAN'T compute: values that depend on runtime state (CSS variables overridden at runtime, theme switching, inherited backgrounds across deep DOM, gradient backgrounds). Flag these as "verify at runtime" and note that the accessibility skill (axe-core) can confirm.
  • Focus styles: visible focus is best verified at runtime; static analysis can confirm :focus-visible rules exist but not whether they're sufficient.
  • Dynamic ARIA: aria-live regions, dynamic role changes, and modal focus traps need runtime testing.

When you can't determine something statically, include the finding under "Needs runtime verification" at the end of the report rather than omitting it.

Runtime verification with Claude Preview (code mode)

If preview_* tools are available and the project has a runnable dev server, always spin one up and run the audit against the live page in addition to static analysis. Runtime evidence is the difference between "may fail contrast" and "fails 1.32:1 on #ffffff over #e0e0e0". Treat it as a first-class step, not a fallback.

When preview is available and the project ships a previewable surface, follow this loop:

  1. Start the dev server: preview_start (read .claude/launch.json first; create a config if needed).
  2. Navigate to each route in scope: preview_eval with window.location.href = '...' (or use anchor URLs the route accepts).
  3. Inject axe-core: most projects don't bundle it. Inject the CDN build at runtime — fetch the script, eval it into window, then call window.axe.run(document, { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] } }). Re-inject after every navigation; SPA route changes preserve window.axe but full reloads do not.
  4. Walk the user flow: preview_click / preview_fill to advance through wizards/forms, then re-run axe at each step. Many violations only appear on later screens (slide 2, error state, post-submit confirmation).
  5. Trigger error and empty states: submit empty forms, send invalid input — check that error messages have role="alert", that inputs gain aria-invalid / aria-describedby, that toasts announce.
  6. Inspect specific elements: preview_inspect for resolved CSS values (real contrast ratios, real aria-label strings), preview_eval for ad-hoc DOM queries (document.querySelectorAll('input[aria-describedby]').length, full radio-group structure, tabindex="-1" audit, etc.).
  7. Capture proof: preview_screenshot for visual evidence on contrast / focus / layout issues — attach the path or name in the report's "To reproduce" steps.

What runtime verification adds beyond static review:

  • Real contrast ratios with the actual CSS variables resolved — moves findings out of "Needs runtime verification" into Critical/Major.
  • Real ARIA tree — confirms whether Mantine/Chakra/Radix wrappers actually emit the expected roles, names, and aria-* attributes (libraries' defaults are easy to assume incorrectly).
  • Real validation behaviour — whether errors have role="alert", whether inputs are wired with aria-describedby, whether modals get an accessible name from a title prop.
  • Real focus orderpreview_eval document.activeElement before/after Tab keypresses to confirm whether tabIndex={-1} skips a control or focus is moved into a modal.
  • Concrete reproduction steps — replace "Tab through the page" with "open http://localhost:8000/sambla-se?slide=1, press Tab three times — focus lands on the chip group, skipping the back button."

What runtime verification still cannot do:

  • Actual screen-reader announcements — axe and DOM inspection get you the expected output, but VoiceOver/NVDA pronunciation, voice-switching on lang, and rotor behaviour need a real assistive-tech pass. State this explicitly in findings instead of claiming "VoiceOver says X" when you only inferred it from the accessibility tree.
  • Visible focus indicator quality — you can verify :focus-visible rules exist, but whether the indicator is perceivable against every background it lands on is a sighted human / contrast-tool task.
  • Reflow at 320 px and 200% zoompreview_resize to 320 width tests reflow; zoom is harder to script. Note in the finding that zoom verification was not performed.
  • Reduced motionpreview_resize accepts colorScheme but not motion preferences directly. Static review (looking for prefers-reduced-motion media queries) usually has to do.

If runtime verification turns up issues that contradict the static review, trust the runtime result and update the finding. The static read missed something — usually a wrapper component or a runtime-injected attribute.

Output rules

Heading hierarchy

# <Page or section name>
## Critical
### <Issue 1 title>
### <Issue 2 title>
## Major
### ...
## Minor
### ...
## AAA (trivial-to-fix)
### ...
## Needs runtime verification
### ...

Group by page/route/component/frame at the top level. Use one block per issue under the severity heading.

Severity definitions

  • Critical — blocks core task completion for users of an assistive technology (e.g. unlabeled submit button, modal that can't be closed by keyboard, missing form labels on the primary flow).
  • Major — significantly degrades experience but task is still completable (e.g. low contrast on body text, missing skip link, heading-order skip).
  • Minor — nuisance or inconsistency (e.g. redundant alt text, decorative image with alt="image", minor heading hierarchy on a non-critical page).

Per-issue fields (CODE mode)

For every issue, include all of these — never omit a field. If a field doesn't apply, write N/A and explain in one sentence.

  1. Title — short descriptive (e.g. "Submit button has no accessible name")
  2. Location — file path(s) with line number(s), formatted as path/to/file.tsx:42 (and a range if multi-line, e.g. :42-48)
  3. Snippet — the offending code verbatim, with 1–2 surrounding lines for context. Do not paraphrase. Use a fenced code block with the language tag.
  4. Why it harms users — plain English. Name the assistive technology or user group affected (screen reader users, keyboard-only users, users with low vision, users with motor impairments, users with cognitive disabilities, users with photosensitivity, etc.).
  5. WCAG criterion — exact format: <number> <Name> — Level <A|AA|AAA> (e.g. 1.4.3 Contrast (Minimum) — Level AA).
  6. Screen reader announcement (for ARIA / labeling / name-role-value issues) — what a screen reader will actually announce, or what it will fail to announce. Example: "NVDA announces 'button' with no name; the user has no idea what activating it does."
  7. Contrast measurement (for contrast issues) — foreground hex, background hex, computed ratio, required ratio. Example: "#7a7a7a on #ffffff = 4.48:1, fails 4.5:1 required for normal text."
  8. To reproduce — exact steps a developer can follow to see the issue and capture a screenshot:
    • Route or URL (e.g. /checkout/payment)
    • Component or selector if not obvious from the route
    • Action (e.g. "tab to the third field", "open with VoiceOver running")
    • Expected vs observed (e.g. "Expected: button announces 'Submit order'. Observed: announces 'button'.")

Per-issue fields (FIGMA mode)

  1. Title
  2. Location — frame name and node path (e.g. Checkout / Payment / Footer / CTA), plus the Figma URL if available
  3. Why it harms users — plain English, name the AT or user group
  4. WCAG criterion — same format as code mode
  5. Contrast measurement (for contrast issues) — same format as code mode
  6. Visual reference — instruction for capturing a screenshot of the frame or node so the issue is documented

(No code snippet field. No screen reader announcement unless ARIA semantics are documented in the design.)

Worked example (CODE mode)

### Submit button has no accessible name

**Location:** src/components/CheckoutForm.tsx:124

**Snippet:**
```tsx
<button type="submit" onClick={handleSubmit}>
  <Icon name="arrow-right" />
</button>

Why it harms users: The button contains only an icon and no text or aria-label. Screen reader users (NVDA, JAWS, VoiceOver) will hear "button" with no description and cannot determine what action it performs. This blocks task completion in the primary checkout flow.

WCAG criterion: 4.1.2 Name, Role, Value — Level A

Screen reader announcement: VoiceOver announces "button"; NVDA announces "button". The user has no way to know this submits the order.

To reproduce:

  1. Run pnpm dev and open http://localhost:3000/checkout
  2. Fill in any test data and tab to the submit button
  3. Activate VoiceOver (Cmd+F5) and listen as focus lands on the button
  4. Expected: "Submit order, button". Observed: "button".

### Style rules

- Quote file paths and line numbers literally — never paraphrase.
- Quote code snippets verbatim — never edit, summarize, or "clean up" the code.
- Use Markdown fenced code blocks with the correct language tag.
- One issue = one block. Never bundle two distinct issues under one heading.
- Order issues within a tier from highest user impact to lowest.
- **Do not propose fixes.** This is a findings-only report. Even if a fix is obvious, omit it.
- If the user explicitly asks for fixes after the report, that's a separate follow-up.

## Report footer

End every report with a summary block:

Summary

  • Critical:
  • Major:
  • Minor:
  • AAA (trivial):
  • Needs runtime verification:
  • Coverage: <which pages/components/frames were audited>
  • Out of scope:

If significant areas of the codebase were not audited (because of scope), state that explicitly so the reader knows the report isn't comprehensive.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment