PageWeave/Design

Skills / Accessibility Audit

8 Skill skillaccessibilitya11ywcagcontrastkeyboardfocusariascreen-reader .md

Accessibility Audit

WCAG 2.2 AA as a release requirement — not an afterthought

Install as SKILL.md

curl -o skills/accessibility-audit/SKILL.md /skills/accessibility-audit.md

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: 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).
/* 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.