Accessibility Audit
Systematic accessibility audits: contrast, keyboard navigation, focus management, ARIA, screen reader semantics, and reduced motion.
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.
/* 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: nonewithout 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).
/* 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-labelon icon-only buttons (the label is the action, not the icon).aria-expandedon toggle buttons (dropdowns, accordions).aria-current="page"on active navigation link.aria-live="polite"on regions that update dynamically (toasts, status messages).aria-describedbylinking 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, oraria-labelledby). - Disabled state:
disabledattribute (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:
requiredattribute + 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-labelledbypointing 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
alttext. - Decorative images:
alt=""(empty, not missing). - Complex images (charts, diagrams): long description in adjacent text or
aria-describedby. - SVG icons:
<title>element oraria-labelon parent.
Tables (Data)
-
<th>for headers withscope="col"orscope="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
- Automated scan: run axe-core or Lighthouse. Fix all critical/serious violations.
- Keyboard walkthrough: Tab through every page. Every interactive element reachable? Focus visible? Order logical?
- Screen reader test: VoiceOver (Mac) or NVDA (Windows). Navigate by headings, links, form controls. Does the page make sense?
- Zoom test: 200% browser zoom. Content reflows? No horizontal scroll? No overlapping text?
- Contrast check: every text/background pair ≥ 4.5:1. Every UI element ≥ 3:1.
- 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: nonewithout replacement focus style.- Placeholder as the only label (disappears on focus, no persistent reference).
color: grayfor body text on near-white backgrounds.- Icon-only buttons without
aria-label. onclickon<div>withoutrole="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.