a11y-audit

Accessibility audit of UI code or a running site against WCAG 2.1 AA. Walks every WCAG criterion that maps to common patterns: keyboard reach, focus visibility, contrast, semantic HTML, ARIA, labels,

SKILL.md

GitHub

/a11y-audit

Walk a UI for accessibility issues against WCAG 2.1 Level AA — the legal bar in most jurisdictions and the right bar everywhere. Output is a severity-graded list with concrete code fixes, not a generic "improve accessibility" lecture.

When to use

  • Before shipping any new UI to production.
  • Before a launch in EU / UK (European Accessibility Act applies June 2025).
  • Before any government / education / healthcare deal — those usually require Section 508 / EN 301 549.
  • After axe-core or Lighthouse flagged issues and you want a complete walk, not just the automated catches.
  • When the user reports a specific accessibility complaint.

Operating method

1. Confirm scope

  • What's the target? Source code (/path/to/component.tsx), a route URL, a running app, or all of the above.
  • What WCAG level? Default AA. AAA only if the user explicitly asks (some criteria have meaningful trade-offs at AAA).
  • What user agents matter? Default: latest Chrome, Safari, Firefox + VoiceOver (Mac/iOS), NVDA (Windows). Add more if the user serves specific audiences.

2. Run the deterministic scans first

Whenever feasible:

  • axe-core via Playwright or browser extension on every route.
  • Lighthouse accessibility audit (Chrome DevTools or lhci).
  • html-validate on rendered output to catch invalid markup that screen readers misinterpret.
  • Color-contrast checker (axe / Stark / WCAG Contrast Checker) on every text/bg pair.

Capture the output. The automated tools catch ~30% of issues — necessary but not sufficient.

3. Walk WCAG 2.1 AA by relevant criteria

These are the criteria that come up in 90% of audits. Walk each that applies.

Perceivable

1.1.1 Non-text Content (A)

  • Every meaningful image has alt. Decorative images have alt="".
  • Icons that convey meaning have aria-label or accompanying text.
  • Charts/graphs have a text alternative or detailed description.

1.3.1 Info and Relationships (A)

  • Form labels associated via <label for=…> or aria-labelledby.
  • Tables use <th> with scope.
  • Headings in order, no skipped levels.
  • Lists marked up as <ul> / <ol>, not styled <div>s.

1.3.2 Meaningful Sequence (A)

  • DOM order matches visual reading order (CSS flexbox / grid order does NOT change reading order for screen readers).

1.3.5 Identify Input Purpose (AA)

  • Common inputs (name, email, address, payment) use correct autocomplete attribute.

1.4.1 Use of Color (A)

  • Color is never the only carrier of meaning. Required fields show "*" or text in addition to red.
  • Error states have an icon + text in addition to red border.

1.4.3 Contrast Minimum (AA)

  • Body text: ≥ 4.5:1 against background.
  • Large text (≥ 18pt regular / 14pt bold): ≥ 3:1.
  • Common gotcha: placeholder text and disabled buttons often fail.

1.4.4 Resize Text (AA)

  • Page works when zoomed to 200%. No content cut off, no horizontal scrolling.

1.4.10 Reflow (AA)

  • Page works at 320px wide and 256px tall without 2D scrolling.

1.4.11 Non-text Contrast (AA)

  • UI components and graphical objects: ≥ 3:1 against adjacent colors. Includes button borders, focus rings, form-input borders.

1.4.12 Text Spacing (AA)

  • Page works when the user overrides line-height to 1.5×, paragraph spacing to 2×, letter-spacing to 0.12em, word-spacing to 0.16em. No content cut off.

1.4.13 Content on Hover or Focus (AA)

  • Tooltips dismissible (Esc), hoverable (mouse can move into them), persistent (don't disappear during read).

Operable

2.1.1 Keyboard (A)

  • Every interactive element reachable by Tab.
  • All actions doable without a mouse.

2.1.2 No Keyboard Trap (A)

  • Once focus enters a component, it can leave by Tab/Shift-Tab/Esc.

2.1.4 Character Key Shortcuts (A)

  • Single-key shortcuts can be turned off, remapped, or only fire on focus.

2.4.1 Bypass Blocks (A)

  • Skip-to-content link as first focusable element.
  • Or proper landmarks (<main>, <nav>, <aside>).

2.4.2 Page Titled (A)

  • Every page has a unique <title>.

2.4.3 Focus Order (A)

  • Tab order matches visual reading order.

2.4.4 Link Purpose (A)

  • Link text describes destination ("Read API docs" not "Click here").

2.4.6 Headings and Labels (AA)

  • Headings describe section content.
  • Form labels describe purpose.

2.4.7 Focus Visible (AA)

  • Focus indicator visible. Never outline: none without a replacement ring.
  • Focus ring contrast ≥ 3:1 against any background.

2.5.5 Target Size (AAA but worth checking) → 2.5.8 Target Size Minimum (AA in 2.2)

  • Touch targets ≥ 24×24 CSS px (WCAG 2.2 AA), ideally ≥ 44×44 (Apple HIG).

Understandable

3.1.1 Language of Page (A)

  • <html lang="en"> (or correct ISO code).

3.1.2 Language of Parts (AA)

  • Foreign-language phrases marked up: <span lang="fr">déjà vu</span>.

3.2.1 On Focus (A)

  • Focusing an element does not change context (no auto-submit, no auto-navigation).

3.2.2 On Input (A)

  • Changing an input's value does not change context (no auto-submit on dropdown change without warning).

3.3.1 Error Identification (A)

  • Errors identified in text, not just by color or icon.

3.3.2 Labels or Instructions (A)

  • Every input has a visible label or programmatic label.

3.3.3 Error Suggestion (AA)

  • Error messages include how to fix the error.

3.3.4 Error Prevention (Legal, Financial, Data) (AA)

  • For irreversible actions: confirm + reversible OR confirm + checkable summary.

Robust

4.1.2 Name, Role, Value (A)

  • Custom interactive components have proper role, name, state.
  • Use semantic HTML (<button>) over <div role="button"> whenever possible.

4.1.3 Status Messages (AA)

  • Status changes (toasts, validation results, loading completion) announced via aria-live.

4. Walk patterns common to most apps

Beyond per-criterion walkthrough, check these patterns:

  • Modals: focus trapped within while open, focus returned on close, Esc closes, click-outside closes (or aria-hidden on background, role="dialog", aria-labelledby on title).
  • Menus / dropdowns: arrow-key navigation, Esc closes, Home/End navigation, type-ahead.
  • Forms: label association, error linkage via aria-describedby, error focus on submit failure, required-field marking.
  • Tabs: arrow-key navigation, role="tablist" + role="tab" + role="tabpanel", aria-selected.
  • Disclosure (accordion / details): aria-expanded, focusable trigger.
  • Carousels: keyboard reachable, autoplay pausable, current slide indicated.
  • Custom controls: any div that responds to clicks should be a button instead.

5. Test with a screen reader (live audit)

If auditing a running app, use a screen reader for at least one critical flow:

  • Mac/iOS: VoiceOver (Cmd+F5 / triple-click home).
  • Windows: NVDA (free) or JAWS.
  • Linux: Orca.

Listen to: page title, landmarks, headings, link list, form fields, error messages. If the experience makes no sense, the audit isn't done.

6. Severity grading

  • Critical — blocks a user with disability from completing a core task. Pre-auth modals with no keyboard access, forms with no labels, color-only error states.
  • High — significantly impairs but workaround exists. Missing skip link, low-contrast body text, missing alt on key images.
  • Medium — friction for some users. Tooltip not dismissible by Esc, missing aria-live on toast.
  • Low — defense-in-depth. Lang attribute on parts, foreign-phrase markup.

7. Output format

## Scope
- Target: <files / routes>
- WCAG level: 2.1 AA
- Tools used: axe-core, Lighthouse, manual VoiceOver walk on /signup, /checkout

## Verdict: PASS | CONDITIONAL | FAIL

## Findings (severity-ordered)

### Critical (N)
1. **No keyboard access to "Add to cart" button** (WCAG 2.1.1).
   - File: `src/components/ProductCard.tsx:42`
   - Issue: `<div onClick={...}>` instead of `<button>`. Not reachable by Tab.
   - Fix:
     ```tsx
     // Before
     <div className="add-btn" onClick={handleAdd}>Add to cart</div>
     // After
     <button type="button" className="add-btn" onClick={handleAdd}>Add to cart</button>
     ```

2. **Modal traps focus on screen readers** (WCAG 2.4.7, 4.1.2).
   - File: `src/components/Modal.tsx:18`
   - Issue: …
   - Fix: use `react-focus-lock` or implement focus trap manually.

### High (N)
…

### Medium (N)
…

### Low (N)
…

## Patterns audited
- [x] Modals — 2 issues
- [x] Forms — 4 issues
- [x] Color contrast — 1 issue
- [x] Keyboard navigation — 3 issues
- [x] Focus visibility — 1 issue
- [x] Skip link — missing
- [x] Heading hierarchy — clean
- [x] Alt text — 2 missing

## Screen reader walk
- [x] /signup with VoiceOver — 1 issue (form errors not announced)
- [x] /checkout with VoiceOver — 0 issues

## Recommended next moves
1. Fix all Critical (1 day).
2. Fix High in priority order (1 week).
3. Add axe-core to CI to catch regressions.
4. Schedule a screen-reader walk per release for top 3 flows.

Boundaries

  • Don't claim "accessible" without a screen-reader walk on at least one flow.
  • Don't write a fix you haven't verified would work — read the actual component code first.
  • Don't audit categories that don't apply (no point checking video captions if there's no video).
  • Don't recommend AAA criteria when the user asked for AA — AAA has trade-offs.
  • Don't ignore the user's specific concerns. If they reported a complaint, prioritize that pattern.
  • Always cite the WCAG criterion (e.g., "WCAG 2.4.7") so the team can search the standard.