Overview

Build to WCAG 2.2 AA on every public page. The criteria below are the AA floor; treat each as a pass/fail gate, not a guideline.

WCAG 2.2 AA checklist

Run through this list before each release:

  • Text contrast >= 4.5:1; large text >= 3:1 (SC 1.4.3)
  • UI component borders and focus rings >= 3:1 against adjacent color (SC 1.4.11)
  • All interactive controls reachable and operable by keyboard (SC 2.1.1)
  • No keyboard traps outside intentional modal dialogs (SC 2.1.2)
  • Skip link as first focusable element (SC 2.4.1)
  • Focus indicator visible on every interactive element (SC 2.4.7)
  • Focus not fully obscured by sticky headers or overlays (SC 2.4.11)
  • Images have meaningful alt; decorative images have alt="" (SC 1.1.1)
  • Every <input> has a programmatic label (SC 1.3.1, SC 3.3.2)
  • Touch and click targets >= 24x24 CSS px, or sufficient spacing (SC 2.5.8)
  • Motion animations respect prefers-reduced-motion (SC 2.3.3)
  • ARIA used only when no native element fits; all ARIA patterns complete

Color contrast: 4.5:1 body, 3:1 large

SC 1.4.3 sets the floor: body text needs 4.5:1 contrast against its background; large text (18pt regular or 14pt bold and above) needs 3:1. SC 1.4.11 extends the rule to interactive component borders and focus rings: they need 3:1 against adjacent colors.

Disabled controls carry no contrast requirement. Disable sparingly; an unusable affordance is worse than a missing one.

Use DevTools color picker or axe-core to confirm. Do not eyeball.

Full keyboard navigation; no traps

SC 2.1.1 requires every interactive control to be reachable and operable by keyboard alone. SC 2.1.2 bans trapping focus outside a modal.

  • Tab and Shift+Tab must reach every control in DOM order.
  • Enter and Space must activate buttons; Enter must follow links.
  • Arrow keys must navigate inside composite widgets (menus, tabs, sliders).
  • Escape must close modals and dismiss menus, returning focus to the trigger.
  • Inside a modal, trap focus within the dialog. Outside a modal, never trap.

DOM order must match visual order. Do not reorder with CSS order or position: absolute and expect tab order to follow.

See html for element-level keyboard semantics and css for layout rules that preserve reading order.

Visible focus indicators

SC 2.4.7 requires a visible focus indicator on every interactive element. SC 2.4.11 (new in WCAG 2.2) requires that the focused component is not fully hidden by author-created content such as sticky banners or cookie bars.

Style focus with :focus-visible, not :focus. The ring appears for keyboard users; pointer clicks stay clean.

:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}

Add a skip link as the first focusable element so keyboard users can jump to <main>:

<a class="skip-link" href="#main">Skip to content</a>

Reveal it on focus, hide it off-screen otherwise. See css for the off-screen clip pattern.

Meaningful alt text; empty alt for decorative images

SC 1.1.1 covers all non-text content. The rule is simple:

  • Informative image: alt describes what the image conveys in context. “Bar chart showing 40% growth in Q2” beats “chart.png”.
  • Decorative image: alt="" so screen readers skip it.
  • Functional image (button icon): alt states the action. “Search”, not “magnifying glass”.
  • Complex image (diagram): alt gives a brief label; a longer description follows in adjacent text or a linked page.

Never leave alt missing. An absent attribute is announced as the file path by many readers.

Semantic HTML first; ARIA sparingly

Use the element that names the role. Every native element carries keyboard behavior, focus management, and an accessible name at zero cost.

  • <button> for actions, never <div> or <span> styled as a button.
  • <a href="..."> for navigation; <a> without href is not a link.
  • <label> paired with every input; see forms.
  • <fieldset> and <legend> to group radios, checkboxes, and address blocks.
  • <nav>, <main>, <header>, <footer>, <aside> for page landmarks.

ARIA does not add behavior; it changes what assistive tech announces. Use it only when no native element fits: custom comboboxes, live regions (aria-live="polite"), icon-only buttons (aria-label="Close"), disclosure widgets (aria-expanded). A half-built ARIA pattern is worse than a <select>.

Programmatic form labels

SC 1.3.1 and SC 3.3.2 require every input to have a label that is programmatically associated, not just visually adjacent.

<!-- explicit label -->
<label for="email">Email address</label>
<input id="email" type="email" />
 
<!-- implicit label -->
<label>
  Email address
  <input type="email" />
</label>

Never rely on placeholder alone as a label; it disappears on input and has poor contrast by default. See forms for full form pattern rules.

Target size: 24x24 CSS px minimum (SC 2.5.8)

SC 2.5.8 is new in WCAG 2.2. Interactive targets must meet one of:

  • Bounding box >= 24x24 CSS px, OR
  • Offset spacing around a smaller target such that a 24x24 px circle centered on the target does not intersect any adjacent target.

Inline text links are exempt. Buttons, checkboxes, and icon controls are not. Apply min-width: 24px; min-height: 24px and ensure adjacent targets have >= 2px spacing.

Respect prefers-reduced-motion (SC 2.3.3)

SC 2.3.3 requires that motion animation triggered by interaction can be disabled unless it is essential. Map this to the CSS media query:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

For JavaScript-driven animations, read window.matchMedia('(prefers-reduced-motion: reduce)').matches and skip or substitute the animation when true.

Automated checking with pa11y

Automated tools catch roughly 30 to 40 percent of WCAG failures. Pair them with one keyboard pass and one screen reader pass per major change; tools miss reading order, focus management, and cognitive load.

Run pa11y against a URL or a local server:

npx pa11y --standard WCAG2AA https://localhost:8080/

See run-pa11y-locally for local setup with a dev server and configure-pa11y-thresholds to set error budgets per route in CI.

For Playwright-based pipelines, @axe-core/playwright integrates directly into test files and surfaces violations as assertion failures.