---
name: accessibility-audit
description: Systematic accessibility audits: contrast, keyboard navigation, focus management, ARIA, screen reader semantics, and reduced motion.
---

# Accessibility Audit

WCAG 2.2 AA as a release requirement — not an afterthought

Make accessibility a release requirement, not a post-launch patch. Every interface must be usable by everyone — keyboard users, screen reader users, low-vision users, and motor-impaired users.

## The Non-Negotiable Rules

### Contrast

- **Body text**: ≥ 4.5:1 against its background. No exceptions.
- **Large text** (≥ 18px or bold ≥ 14px): ≥ 3:1.
- **Placeholder text**: same 4.5:1 as body — not the muted-gray default.
- **UI components** (buttons, inputs, icons): ≥ 3:1 against adjacent colors.
- **Focus indicators**: ≥ 3:1 against both background and adjacent colors.

The most common failure: muted gray body text on a tinted near-white. If contrast is even close, bump the body color toward the ink end of the ramp.

```css
/* BAD: 3.8:1 — fails WCAG AA */
--text-muted: oklch(0.65 0.01 240);

/* GOOD: 5.2:1 — passes WCAG AA */
--text-muted: oklch(0.5 0.01 240);
```

### Keyboard Navigation

Every interactive element must be reachable and operable via keyboard:

- **Tab order** follows visual order. No `tabindex` > 0.
- **Focus visible**: every focusable element has a visible focus ring. Never `outline: none` without replacement.
- **Focus trap**: modals and dialogs trap focus. Escape closes. Focus returns to trigger on close.
- **Skip links**: first focusable element on page is a "Skip to main content" link.
- **No keyboard traps**: focus can always move forward (Tab) and backward (Shift+Tab).

```css
/* Focus ring that works on any background */
:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
```

### Semantic HTML

Use the right element for the job:

| Use | Not |
|-----|-----|
| `<button>` | `<div onclick>` |
| `<a href>` | `<span onclick>` |
| `<nav>` | `<div class="nav">` |
| `<main>` | `<div id="main">` |
| `<h1>`–`<h6>` | `<p class="heading">` |
| `<ul>/<ol>` | `<div>` with bullet characters |
| `<table>` | CSS grid mimicking a table |

Semantic HTML gives you keyboard behavior, ARIA roles, and screen reader announcements for free.

### ARIA — Only When HTML Isn't Enough

ARIA supplements HTML, never replaces it. The first rule of ARIA: don't use ARIA if a native HTML element provides the semantics.

**Essential ARIA patterns:**

- `aria-label` on icon-only buttons (the label is the action, not the icon).
- `aria-expanded` on toggle buttons (dropdowns, accordions).
- `aria-current="page"` on active navigation link.
- `aria-live="polite"` on regions that update dynamically (toasts, status messages).
- `aria-describedby` linking inputs to their error messages.
- `role="alert"` on error messages (announced immediately by screen readers).

**Never:**
- `aria-hidden="true"` on visible content.
- `role="button"` on a `<button>` (redundant).
- ARIA labels that duplicate visible text.

## Component Accessibility Checklist

### Buttons & Links

- [ ] Keyboard-focusable (Tab to reach, Enter/Space to activate).
- [ ] Visible focus indicator (≥ 3:1 contrast).
- [ ] Accessible name (text content, `aria-label`, or `aria-labelledby`).
- [ ] Disabled state: `disabled` attribute (not just visual styling).
- [ ] Loading state: `aria-busy="true"` + `aria-disabled="true"`.

### Forms

- [ ] Every input has a visible `<label>` (not just placeholder).
- [ ] Error messages linked via `aria-describedby`.
- [ ] Required fields: `required` attribute + visual indicator.
- [ ] Error state: `aria-invalid="true"` on the input.
- [ ] Group related inputs: `<fieldset>` + `<legend>`.
- [ ] Custom selects: use native `<select>` or implement full listbox pattern.

### Modals & Dialogs

- [ ] Focus trap: Tab cycles within modal.
- [ ] Escape closes.
- [ ] `role="dialog"` + `aria-modal="true"`.
- [ ] `aria-labelledby` pointing to the title.
- [ ] Focus moves to first focusable element on open.
- [ ] Focus returns to trigger on close.
- [ ] Backdrop click closes (with `aria-label="Close"` on backdrop if applicable).

### Images

- [ ] Informative images: descriptive `alt` text.
- [ ] Decorative images: `alt=""` (empty, not missing).
- [ ] Complex images (charts, diagrams): long description in adjacent text or `aria-describedby`.
- [ ] SVG icons: `<title>` element or `aria-label` on parent.

### Tables (Data)

- [ ] `<th>` for headers with `scope="col"` or `scope="row"`.
- [ ] `<caption>` describing the table's purpose.
- [ ] No merged cells unless absolutely necessary (screen reader confusion).

## Touch & Motor

- **Touch targets**: minimum 44×44px (WCAG 2.2 AA). 48×48px for Android.
- **Spacing between targets**: minimum 8px gap to prevent accidental taps.
- **Drag interactions**: always provide an alternative non-drag interaction.
- **Timeouts**: give users control over time limits. No auto-redirect without warning.

## Motion & Animation

- `prefers-reduced-motion: reduce` → crossfade or instant transition.
- No auto-playing animations without user control.
- No content that flashes more than 3 times per second (seizure risk).
- Scroll-triggered animations must have a non-animated fallback.

## Color & Visual

- **Never use color alone** to convey information. Pair with text, icon, or pattern.
- **Links**: distinguishable from surrounding text without color alone (underline, bold, or different weight).
- **Focus indicators**: visible on all backgrounds. Test on light AND dark themes.
- **Text resizing**: layout must hold at 200% browser zoom.

## Audit Workflow

1. **Automated scan**: run axe-core or Lighthouse. Fix all critical/serious violations.
2. **Keyboard walkthrough**: Tab through every page. Every interactive element reachable? Focus visible? Order logical?
3. **Screen reader test**: VoiceOver (Mac) or NVDA (Windows). Navigate by headings, links, form controls. Does the page make sense?
4. **Zoom test**: 200% browser zoom. Content reflows? No horizontal scroll? No overlapping text?
5. **Contrast check**: every text/background pair ≥ 4.5:1. Every UI element ≥ 3:1.
6. **Motion check**: toggle `prefers-reduced-motion`. All animations degrade gracefully?

## Severity Levels

| Level | Meaning | Fix timeline |
|-------|---------|-------------|
| P0 | Blocks task completion | Before merge |
| P1 | Degrades experience significantly | Before release |
| P2 | Minor issue, workaround exists | Next sprint |

## Common Anti-Patterns

- `outline: none` without replacement focus style.
- Placeholder as the only label (disappears on focus, no persistent reference).
- `color: gray` for body text on near-white backgrounds.
- Icon-only buttons without `aria-label`.
- `onclick` on `<div>` without `role="button"`, `tabindex="0"`, and keyboard handler.
- Modals without focus trap.
- Links that look like buttons (or vice versa) — visual form should match function.
- `target="_blank"` without warning (opens new tab/window unexpectedly).
- Auto-playing video/audio without mute toggle.
- Custom dropdowns that aren't keyboard-navigable.