---
title: "Accessibility"
slug: "accessibility"
category: "frontend"
tags: ["frontend", "accessibility", "wcag", "aria"]
status: "stable"
last_updated: 2026-06-15
summary: "WCAG 2.2 AA checklist: contrast ratios, keyboard nav, focus indicators, alt text, ARIA, form labels, target size, reduced-motion."
related: ["[[frontend/html]]", "[[frontend/css]]", "[[frontend/forms]]", "[[howto/run-pa11y-locally]]", "[[howto/configure-pa11y-thresholds]]", "[[seo/technical]]"]
---

## 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 [[frontend/html]] for element-level keyboard semantics and [[frontend/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.

```css
: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>`:

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

Reveal it on focus, hide it off-screen otherwise. See [[frontend/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 [[frontend/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.

```html
<!-- 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 [[frontend/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:

```css
@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:

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

See [[howto/run-pa11y-locally]] for local setup with a dev server and [[howto/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.

## Related

- [[frontend/html]]
- [[frontend/css]]
- [[frontend/forms]]
- [[howto/run-pa11y-locally]]
- [[howto/configure-pa11y-thresholds]]
- [[seo/technical]]
