Frontend No. 047Editor's pick

Accessibility Audit skill for Claude Code

WCAG 2.2 AA findings you can hand to engineers. Claude runs the audit a specialist would: axe-core and Lighthouse first, then the manual passes automation cannot judge — keyboard order and traps, focus visibility and obscuring, contrast in both themes via a bundled script, ARIA widget behavior, reflow, target size and a screen reader walk through critical flows. Every finding cites its success criterion, location, impact and fix.

  • nameaccessibility-audit
  • size208 lines · ≈2,300 tokens
  • files3
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Audits web pages and components against WCAG 2.2 Level AA using automated scans (axe-core, Lighthouse) plus the manual checks automation misses — keyboard operation, focus visibility and order, contrast, semantics, ARIA widget patterns, reflow, target size, and screen reader behavior — and writes a prioritized findings report with success criteria and fixes. Use when asked for an accessibility or a11y audit, WCAG or ADA/EAA compliance check, VPAT prep, axe violations, keyboard traps, or screen reader bugs.

Try asking

  • Run a WCAG 2.2 AA audit on our checkout flow and write up the findings
  • Our VPAT is due — map the current issues to success criteria
  • Is this custom dropdown accessible? Check it against the ARIA patterns

Use it when

  • You need an accessibility audit or compliance check before launch
  • Preparing a VPAT, ACR or EAA conformance statement
  • axe reports violations and you need them prioritized and fixed
  • Users report keyboard or screen reader problems

Skip it when

How to check the result

Re-run the axe scan and the exact manual steps from each finding after fixes, and add Playwright axe checks so the fixed templates fail CI if they regress.

The common mistake it prevents

Declaring a page accessible because axe shows zero violations, when keyboard traps, focus order and screen reader announcements were never tested.

~/.claude/skills/accessibility-audit/SKILL.md

--- name: accessibility-audit description: Audits web pages and components against WCAG 2.2 Level AA using automated scans (axe-core, Lighthouse) plus the manual checks automation… license: MIT ---

An audit is done when every page template in scope has passed an automated scan, a keyboard-only walkthrough, a contrast check, and a screen reader pass, and each failure is written up with the success criterion, exact location, user impact, and a concrete fix. Automated tools find roughly a third of issues; the manual passes are not optional.

Scope first

Agree on and record before testing:

  • URLs or component list, and the representative templates (home, listing, detail, form, checkout, modal, authenticated dashboard).
  • Target: WCAG 2.2 AA (covers EN 301 549 / EAA and most ADA settlements in practice).
  • Browsers and assistive tech: VoiceOver + Safari (macOS/iOS), NVDA + Chrome or Firefox (Windows). TalkBack + Chrome for Android when mobile web matters.
  • States to cover: logged in/out, empty, error, loading, expanded menus, open dialogs.

Workflow

- [ ] 1. Automated scan of every template and key state
- [ ] 2. Keyboard-only walkthrough
- [ ] 3. Focus visibility, order, and obscuring
- [ ] 4. Contrast: text, UI components, focus indicators, both themes
- [ ] 5. Semantics: landmarks, headings, names, roles, states
- [ ] 6. Custom widgets against ARIA APG patterns
- [ ] 7. Zoom, reflow, text spacing, target size, motion
- [ ] 8. Screen reader pass on critical flows
- [ ] 9. Write the report; propose fixes in priority order

1. Automated scan

# Quick CLI scan of a running site
npx @axe-core/cli http://localhost:3000 http://localhost:3000/checkout \
  --tags wcag2a,wcag2aa,wcag21a,wcag21aa,wcag22aa --exit
# Lighthouse accessibility category only
npx lighthouse http://localhost:3000 --only-categories=accessibility --output=json --output-path=./a11y.json

For states behind interaction, scan inside Playwright tests:

import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

test("checkout dialog has no axe violations", async ({ page }) => {
  await page.goto("/cart");
  await page.getByRole("button", { name: "Checkout" }).click();
  const results = await new AxeBuilder({ page })
    .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"])
    .analyze();
  expect(results.violations).toEqual([]);
});

Treat “needs review” items as manual checks, not passes.

2. Keyboard walkthrough

Unplug the mouse mentally. For each template:

  • Tab/Shift+Tab reaches every interactive element, in an order matching the visual reading order (2.4.3). Nothing non-interactive receives focus.
  • A skip link is the first focusable element and moves focus to <main> (2.4.1).
  • Enter activates links and buttons; Space activates buttons, checkboxes; arrow keys operate radios, tabs, menus, listboxes, sliders per APG.
  • Esc closes dialogs, menus, and popovers and returns focus to the trigger.
  • No keyboard trap (2.1.2): you can always leave a widget with Tab or Esc.
  • Hover-only content (tooltips, mega menus) is reachable by focus and dismissible without moving focus (1.4.13).

3. Focus

  • Every focused element has a visible indicator (2.4.7). Check the code for outline: none without a replacement: grep -rn "outline: *none\|outline: *0" src.
  • Focused elements are not fully hidden by sticky headers, cookie banners, or chat widgets (2.4.11 Focus Not Obscured, new in 2.2). Fix with scroll-padding-top.
  • Opening a modal moves focus into it; closing returns focus to the opener. Content behind is inert (<dialog> with showModal() does both natively).
  • Route changes in SPAs move focus to the new page’s <h1> or main region and update document.title.

4. Contrast

ContentMinimum ratio (AA)
Body text under 24 px (or under 18.66 px bold)4.5:1 (1.4.3)
Large text ≥ 24 px, or ≥ 18.66 px bold3:1
UI component boundaries, icons conveying meaning, focus indicators, chart lines3:1 against adjacent colors (1.4.11)
Disabled controls, pure decoration, logosExempt

Check computed colors, including text over images/gradients (test the worst spot) and both light and dark themes. Use the bundled script for pairs pulled from tokens or devtools:

python3 scripts/contrast_check.py "#6b7280" "#ffffff"
python3 scripts/contrast_check.py --file pairs.txt --min 4.5     # lines: fg bg [label]

It exits non-zero when any pair falls below the threshold and composites colors with alpha over the background.

5. Semantics

  • One <h1> per page; headings do not skip levels for styling (1.3.1).
  • Landmarks: <header>, <nav aria-label>, <main> (exactly one), <footer>. Multiple navs get distinct labels.
  • Every control has an accessible name that contains its visible label (2.5.3 Label in Name). Icon buttons: aria-label or visually hidden text.
  • Images: meaningful alt; decorative alt=""; complex charts get a text alternative.
  • Links describe their destination (“Download invoice PDF”, not “Click here”).
  • Buttons do actions, links navigate. <div onclick> is a finding (4.1.2).
  • lang on <html> and on passages in other languages (3.1.1, 3.1.2).
  • Status messages (cart updated, saved, search results count) use role="status" or aria-live="polite" without moving focus (4.1.3).

6. Custom widgets

Compare each against the WAI-ARIA Authoring Practices pattern: dialog, disclosure, accordion, tabs, menu button, combobox, listbox, tooltip, carousel. Prefer native elements (<dialog>, <details>, <select>, popover attribute) over ARIA rebuilds. Common failures:

  • role="menu" used for site navigation (it promises app-menu keyboard behavior).
  • aria-expanded not toggled; aria-selected on tabs not updated.
  • Combobox without aria-activedescendant or arrow-key support.
  • aria-hidden="true" on an element that contains focusable children.

7. Zoom, reflow, spacing, targets, motion

  • 200% browser zoom: no loss of content or function (1.4.4).
  • 320 CSS px wide viewport (1280 px at 400%): no horizontal scrolling except for data tables, maps, code (1.4.10).
  • Apply text spacing overrides (line-height 1.5, paragraph spacing 2em, letter spacing 0.12em, word spacing 0.16em): no clipped or overlapping text (1.4.12).
  • Pointer targets at least 24×24 CSS px or with enough spacing (2.5.8, new in 2.2).
  • Drag-only interactions (sortable lists, sliders, maps) have a single-pointer alternative (2.5.7, new in 2.2).
  • Auto-playing motion over 5 s can be paused (2.2.2); animations respect prefers-reduced-motion.

8. Screen reader pass

Run the two or three critical flows (sign up, search → product → add to cart → checkout) with VoiceOver (Cmd+F5) and NVDA:

  • Navigate by headings and landmarks; the outline makes sense.
  • Every control announces name, role, state (“Subscribe, checkbox, not checked”).
  • Form errors are announced and associated with fields.
  • Dynamic updates (toasts, live search results) are announced once, not repeatedly.

WCAG 2.2 notes

  • New AA criteria vs. 2.1: 2.4.11 Focus Not Obscured (Minimum), 2.5.7 Dragging Movements, 2.5.8 Target Size (Minimum); new A: 3.2.6 Consistent Help, 3.3.7 Redundant Entry; new AA: 3.3.8 Accessible Authentication (no cognitive tests without alternatives; allow paste and password managers).
  • 4.1.1 Parsing is removed in 2.2; do not report duplicate IDs under it (report under 1.3.1 or 4.1.2 if they break relationships).

For the full A/AA criterion list with a test method for each, read references/wcag-22-checklist.md when producing a conformance report or VPAT, or when a finding’s criterion is unclear.

Severity

SeverityDefinitionExample
CriticalBlocks a task for a group of usersCheckout button unreachable by keyboard; modal traps focus
SeriousTask possible but very hard; major info lossForm errors not announced; 2.8:1 body text
ModerateFriction or confusionHeading levels skipped; vague link text
MinorPolishRedundant title attributes

Report template

# Accessibility audit — <product>, <date>
**Standard:** WCAG 2.2 AA  **Scope:** 7 templates, 3 flows  **AT:** VoiceOver/Safari 18, NVDA 2025/Chrome
**Summary:** 4 critical, 9 serious, 12 moderate, 5 minor. Top risks: keyboard access to checkout, form error announcements.

## Findings
### A-01 — Critical — 2.1.1 Keyboard
**Where:** /checkout, "Place order" (`button.cta-submit`, a `<div>`)
**Impact:** Keyboard and switch users cannot complete purchase.
**Steps:** Tab through the page; the control never receives focus.
**Fix:** Use `<button type="submit">`; remove the click handler on the div.
**Verify:** Reachable by Tab, activates with Enter and Space; axe rule passes.

## Passed checks / not tested
- Reflow at 320 px: pass on all templates
- Not tested: PDF downloads, third-party chat widget (vendor responsibility, noted)

Verification of fixes

  • Re-run the axe scan and the specific manual step from each finding.
  • Add Playwright + axe checks for fixed templates so regressions fail CI.
  • Never mark a finding fixed based on code review alone; reproduce the original steps.

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

references/wcag-22-checklist.md 93 lines · 6.7 KB
# WCAG 2.2 Level A and AA checklist with test methods

Read this when writing a conformance report or VPAT, or when mapping a finding to its
success criterion. "Auto" means axe/Lighthouse catch common failures; "Manual" means
they cannot judge it. Criteria marked (2.2) are new in WCAG 2.2.

## 1. Perceivable

| SC | Level | Requirement | How to test |
|---|---|---|---|
| 1.1.1 Non-text Content | A | Images, icons, controls have text alternatives; decorative ones are hidden | Auto for missing alt; Manual for alt quality and decorative choices |
| 1.2.1 Audio-only and Video-only (Prerecorded) | A | Transcript or audio description alternative | Manual |
| 1.2.2 Captions (Prerecorded) | A | Synchronized captions for video with audio | Manual: play media |
| 1.2.3 Audio Description or Media Alternative | A | Description or full text alternative | Manual |
| 1.2.4 Captions (Live) | AA | Live captions | Manual |
| 1.2.5 Audio Description (Prerecorded) | AA | Audio description for meaningful visuals | Manual |
| 1.3.1 Info and Relationships | A | Structure conveyed in markup: headings, lists, tables, labels, groups | Auto partial; Manual review of headings, table headers, fieldsets |
| 1.3.2 Meaningful Sequence | A | DOM order matches reading order | Manual: disable CSS or use screen reader linear read |
| 1.3.3 Sensory Characteristics | A | Instructions do not rely only on shape, position, sound | Manual: read instructions |
| 1.3.4 Orientation | AA | Works in portrait and landscape | Manual: rotate device/emulator |
| 1.3.5 Identify Input Purpose | AA | Correct `autocomplete` tokens on personal-data fields | Auto partial; Manual check tokens |
| 1.4.1 Use of Color | A | Color is not the only signal (links in text, errors, charts) | Manual; grayscale filter |
| 1.4.2 Audio Control | A | Auto-playing audio over 3 s can be paused/muted | Manual |
| 1.4.3 Contrast (Minimum) | AA | 4.5:1 text, 3:1 large text | Auto partial; script for overlays, images, states |
| 1.4.4 Resize Text | AA | 200% zoom without loss | Manual: browser zoom |
| 1.4.5 Images of Text | AA | Real text instead of text in images | Manual |
| 1.4.10 Reflow | AA | No 2-D scroll at 320 CSS px width | Manual: 320 px viewport |
| 1.4.11 Non-text Contrast | AA | 3:1 for UI boundaries, states, focus, meaningful graphics | Manual + script |
| 1.4.12 Text Spacing | AA | No loss with increased spacing overrides | Manual: apply spacing bookmarklet/stylesheet |
| 1.4.13 Content on Hover or Focus | AA | Dismissible (Esc), hoverable, persistent | Manual |

## 2. Operable

| SC | Level | Requirement | How to test |
|---|---|---|---|
| 2.1.1 Keyboard | A | All functionality operable by keyboard | Manual walkthrough |
| 2.1.2 No Keyboard Trap | A | Focus can always move away | Manual |
| 2.1.4 Character Key Shortcuts | A | Single-key shortcuts can be turned off or remapped, or only active on focus | Manual |
| 2.2.1 Timing Adjustable | A | Time limits can be extended/disabled (session timeouts warn) | Manual |
| 2.2.2 Pause, Stop, Hide | A | Moving/auto-updating content over 5 s can be paused | Manual |
| 2.3.1 Three Flashes or Below Threshold | A | No content flashes more than 3 times per second | Manual |
| 2.4.1 Bypass Blocks | A | Skip link or landmarks | Manual + Auto partial |
| 2.4.2 Page Titled | A | Unique, descriptive `<title>` (also after SPA navigation) | Auto for presence; Manual for quality |
| 2.4.3 Focus Order | A | Logical focus order | Manual |
| 2.4.4 Link Purpose (In Context) | A | Link text plus context identifies destination | Manual; screen reader links list |
| 2.4.5 Multiple Ways | AA | More than one way to find pages (nav, search, sitemap) | Manual |
| 2.4.6 Headings and Labels | AA | Headings and labels describe content | Manual |
| 2.4.7 Focus Visible | AA | Visible focus indicator | Manual |
| 2.4.11 Focus Not Obscured (Minimum) (2.2) | AA | Focused element not entirely hidden by author content | Manual: tab with sticky headers/banners open |
| 2.5.1 Pointer Gestures | A | Multipoint/path gestures have single-pointer alternative | Manual |
| 2.5.2 Pointer Cancellation | A | Actions fire on up-event or can be aborted | Manual |
| 2.5.3 Label in Name | A | Accessible name contains visible label text | Auto partial; Manual |
| 2.5.4 Motion Actuation | A | Shake/tilt features have UI alternative and can be disabled | Manual |
| 2.5.7 Dragging Movements (2.2) | AA | Drag operations have single-pointer alternative | Manual |
| 2.5.8 Target Size (Minimum) (2.2) | AA | Targets ≥ 24×24 CSS px or spaced; inline links exempt | Manual: measure in devtools |

## 3. Understandable

| SC | Level | Requirement | How to test |
|---|---|---|---|
| 3.1.1 Language of Page | A | `lang` on `<html>` | Auto |
| 3.1.2 Language of Parts | AA | `lang` on foreign-language passages | Manual |
| 3.2.1 On Focus | A | Focus does not trigger context change | Manual |
| 3.2.2 On Input | A | Changing a control does not unexpectedly change context (auto-submit selects) | Manual |
| 3.2.3 Consistent Navigation | AA | Repeated nav in same order across pages | Manual |
| 3.2.4 Consistent Identification | AA | Same function, same label across pages | Manual |
| 3.2.6 Consistent Help (2.2) | A | Help mechanisms in same relative place across pages | Manual |
| 3.3.1 Error Identification | A | Errors identified and described in text | Manual: submit invalid data |
| 3.3.2 Labels or Instructions | A | Inputs have labels/instructions | Auto partial; Manual |
| 3.3.3 Error Suggestion | AA | Suggest how to fix when known | Manual |
| 3.3.4 Error Prevention (Legal, Financial, Data) | AA | Reversible, checked, or confirmed submissions | Manual |
| 3.3.7 Redundant Entry (2.2) | A | Previously entered info is auto-populated or selectable | Manual: walk multi-step flows |
| 3.3.8 Accessible Authentication (Minimum) (2.2) | AA | No cognitive function test without alternative; paste and password managers allowed | Manual: try paste in login, check CAPTCHA alternatives |

## 4. Robust

| SC | Level | Requirement | How to test |
|---|---|---|---|
| 4.1.2 Name, Role, Value | A | Custom controls expose name, role, state; updates announced | Auto partial; Manual with screen reader |
| 4.1.3 Status Messages | AA | Status updates announced without focus change | Manual with screen reader |

4.1.1 Parsing was removed in WCAG 2.2 (treated as always satisfied).

## Conformance report row format

```markdown
| Criterion | Conformance | Remarks |
|---|---|---|
| 1.4.3 Contrast (Minimum) | Partially Supports | Placeholder text in search (2.85:1) and disabled-looking secondary buttons (3.9:1) fail; see A-04, A-07 |
| 2.4.11 Focus Not Obscured (Minimum) | Supports | Sticky header uses scroll-padding; verified on all templates |
```

Use the VPAT terms: Supports, Partially Supports, Does Not Support, Not Applicable.
scripts/contrast_check.py 118 lines · 3.9 KB
#!/usr/bin/env python3
"""WCAG 2.x contrast ratio checker (stdlib only).

Usage:
  python3 contrast_check.py FG BG [--min 4.5]
  python3 contrast_check.py --file pairs.txt [--min 4.5]

Colors: #rgb, #rrggbb, #rrggbbaa, rgb(r, g, b), rgba(r, g, b, a).
A foreground with alpha is composited over the background before measuring.
pairs.txt: one pair per line, "FG BG [label...]"; blank lines and lines starting with "//" or "# " are ignored.
Exit code 1 if any pair is below --min, 2 on bad input.
"""
import argparse
import re
import sys


def parse_color(s: str):
    s = s.strip().lower()
    m = re.fullmatch(r"#([0-9a-f]{3,8})", s)
    if m:
        h = m.group(1)
        if len(h) in (3, 4):
            h = "".join(c * 2 for c in h)
        if len(h) not in (6, 8):
            raise ValueError(f"bad hex color: {s}")
        r, g, b = (int(h[i:i + 2], 16) for i in (0, 2, 4))
        a = int(h[6:8], 16) / 255 if len(h) == 8 else 1.0
        return r, g, b, a
    m = re.fullmatch(r"rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+%?))?\s*\)", s)
    if m:
        r, g, b = (float(m.group(i)) for i in (1, 2, 3))
        a = m.group(4)
        if a is None:
            alpha = 1.0
        elif a.endswith("%"):
            alpha = float(a[:-1]) / 100
        else:
            alpha = float(a)
        return r, g, b, alpha
    raise ValueError(f"unsupported color: {s} (use hex or rgb/rgba)")


def composite(fg, bg):
    r, g, b, a = fg
    br, bgc, bb, _ = bg
    return (r * a + br * (1 - a), g * a + bgc * (1 - a), b * a + bb * (1 - a))


def luminance(rgb):
    def chan(c):
        c = c / 255
        return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
    r, g, b = (chan(c) for c in rgb)
    return 0.2126 * r + 0.7152 * g + 0.0722 * b


def ratio(fg_s: str, bg_s: str) -> float:
    fg, bg = parse_color(fg_s), parse_color(bg_s)
    if bg[3] < 1:
        raise ValueError(f"background must be opaque: {bg_s}")
    l1, l2 = luminance(composite(fg, bg)), luminance(bg[:3])
    hi, lo = max(l1, l2), min(l1, l2)
    return (hi + 0.05) / (lo + 0.05)


def verdicts(r: float) -> str:
    def mark(ok):
        return "pass" if ok else "FAIL"
    return (f"AA text {mark(r >= 4.5)} | AA large/UI {mark(r >= 3)} | "
            f"AAA text {mark(r >= 7)} | AAA large {mark(r >= 4.5)}")


def main() -> int:
    ap = argparse.ArgumentParser(description="WCAG contrast checker")
    ap.add_argument("fg", nargs="?")
    ap.add_argument("bg", nargs="?")
    ap.add_argument("--file", help="file with 'FG BG [label]' per line")
    ap.add_argument("--min", type=float, default=4.5, help="required ratio (default 4.5)")
    args = ap.parse_args()

    pairs = []
    if args.file:
        with open(args.file, encoding="utf-8") as fh:
            for n, line in enumerate(fh, 1):
                line = line.strip()
                # comments: lines starting with "//" or "# " (a bare "#abc" is a color)
                if not line or line.startswith("//") or line == "#" or line.startswith("# "):
                    continue
                parts = re.findall(r"rgba?\([^)]*\)|\S+", line)
                if len(parts) < 2:
                    print(f"line {n}: need FG and BG", file=sys.stderr)
                    return 2
                pairs.append((parts[0], parts[1], " ".join(parts[2:]) or f"line {n}"))
    elif args.fg and args.bg:
        pairs.append((args.fg, args.bg, ""))
    else:
        ap.print_usage(sys.stderr)
        return 2

    failures = 0
    for fg, bg, label in pairs:
        try:
            r = ratio(fg, bg)
        except ValueError as e:
            print(f"error: {e}", file=sys.stderr)
            return 2
        status = "OK  " if r >= args.min else "LOW "
        failures += r < args.min
        tag = f" [{label}]" if label else ""
        print(f"{status}{r:5.2f}:1  {fg} on {bg}{tag}  ({verdicts(r)})")

    print(f"\n{len(pairs)} pair(s), {failures} below {args.min}:1")
    return 1 if failures else 0


if __name__ == "__main__":
    sys.exit(main())

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc