cd ../writing
// accessibility · WCAG 2.2

The accessibility cheatsheet — what actually ships.

Accessibility tutorials skew abstract. WCAG documentation is 500 pages. Most developers learn a11y by absorbing the wrong patterns from Stack Overflow. This is the working reference — the techniques that actually pass audits, the ARIA attributes that aren't misused, and the keyboard interactions every interactive element needs.

WCAG 2.2 aligned 10 patterns 0 theory © use freely

01Why this matters now

The U.S. ADA, EU Accessibility Act (effective June 2025), and UK Equality Act all carry real legal teeth in 2026. Lawsuit volume against inaccessible sites has tripled in the last three years. Beyond legal risk, accessibility is just good engineering — the same patterns that help screen reader users help keyboard power users, mobile users, and users on slow connections.

The bar isn't perfection. WCAG 2.2 AA compliance is the working standard, and it's achievable with about ten patterns applied consistently.

02Semantic HTML is 80% of the work

Before any ARIA, use the right HTML element. A semantic element gives you keyboard handling, screen reader announcements, and focus styles for free. Custom div-based widgets give you none of those.

✗ requires JS for everything
<div class="btn" onclick="submit()">Submit</div>
<div class="input" contenteditable></div>
✓ keyboard + screen reader free
<button type="submit">Submit</button>
<input type="text" name="q">

The semantic version is automatically focusable, has Enter/Space activation, announces correctly to screen readers, and has a visible focus ring. The div version gives you none of that without 30 lines of JavaScript.

03The five ARIA rules

ARIA exists for cases HTML alone can't express. Five rules govern its correct use:

  1. Don't use ARIA if HTML can do it. <button> beats role="button" always.
  2. Don't change semantics. Never put role="button" on a heading. Use the right element.
  3. Every interactive control must be keyboard-accessible. Custom controls need keyboard handlers.
  4. Don't hide focusable elements with aria-hidden. If it's focusable but hidden from screen readers, sighted keyboard users will land on a "ghost" element.
  5. Interactive elements need accessible names. Buttons with only icons need aria-label.

04Forms — the rules that matter

✓ properly associated form fields
<label for="email">Email address</label>
<input
  id="email"
  type="email"
  name="email"
  autocomplete="email"
  required
  aria-describedby="email-help">
<p id="email-help">We never share your email.</p>

Three things make this work: label[for] ties the label to the input (clicking the label focuses the input, screen readers announce it). autocomplete lets password managers fill it correctly. aria-describedby ties the help text so it's read after the label.

Common mistake: using placeholder text instead of a label. Placeholders disappear when typing, fail contrast checks, and aren't announced by screen readers. Always use real labels — hide them visually with .sr-only if your design doesn't have space.

05Color contrast — the numbers

WCAG 2.2 contrast minimums:

  • 4.5:1 for body text (under 18px regular, or 14px bold)
  • 3:1 for large text (18px+ regular, or 14px+ bold)
  • 3:1 for UI components (button borders, form field outlines, focus indicators)
  • 3:1 for graphical objects (icons that convey information)

Test with the browser's DevTools color picker (Chrome and Firefox both show contrast ratios directly). The 3:1 rule for UI is new in 2.2 and is the one most sites still fail — make sure your focus ring and form borders have enough contrast against the page background.

06Focus management — the rules

Every interactive element must show a visible focus indicator. The browser default works fine; only override it if you have something better.

✓ visible focus, no jank
/* Show focus only on keyboard navigation, not mouse clicks */
button:focus-visible,
a:focus-visible,
input:focus-visible {
  outline: 2px solid var(--pink);
  outline-offset: 2px;
}

/* Never do this: removes focus everywhere */
*:focus { outline: none; } /* ✗ accessibility violation */

:focus-visible shows focus only on keyboard navigation (Tab key), not when clicking with a mouse. This was the missing piece for years — designers hated visible focus on click, accessibility advocates needed it on Tab. :focus-visible solves both.

07Keyboard interactions — the patterns

Every custom widget needs the right keyboard handlers. The patterns:

  • Buttons: Enter or Space activates.
  • Links: Enter activates (not Space — that scrolls the page).
  • Checkboxes / radios: Space toggles. Arrow keys move between radio buttons in the same group.
  • Tabs: Arrow keys move between tabs. Home/End jump to first/last.
  • Modals: Escape closes. Focus must move into the modal on open, return to trigger on close. Focus trap inside the modal while open.
  • Menus: Arrow keys navigate. Escape closes. Tab moves to next focusable element after the menu (not inside it).

Modals are the most-failed widget. The complete pattern:

✓ accessible modal with native dialog
<button type="button" id="trigger">Open settings</button>

<dialog id="modal" aria-labelledby="title">
  <h2 id="title">Settings</h2>
  <p>Modal content here.</p>
  <button type="button" id="close">Close</button>
</dialog>

<script>
trigger.addEventListener('click', () => modal.showModal());
close.addEventListener('click', () => modal.close());
</script>

The native <dialog> element handles focus trap, Escape-to-close, and inert background — all of which used to require 100+ lines of JavaScript. It ships in every browser as of 2023. Use it.

09Images and alt text

Three rules:

  • Decorative image: alt="" (empty, not omitted). Screen readers skip it.
  • Informative image: describe the information, not the image. "Bar chart" is wrong. "Sales increased 30% in Q2" is right.
  • Functional image (link or button): describe the destination/action, not the image. The image of a magnifying glass icon's alt should be "Search," not "Magnifying glass."

10The .sr-only utility

Sometimes you need text that's invisible visually but read by screen readers (icon button labels, context for screen readers, skip links). The standard utility:

✓ visually hidden but accessible
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

<!-- Usage -->
<button>
  <svg>...</svg>
  <span class="sr-only">Close menu</span>
</button>

11How to actually test

Three free tools, used together, catch 90%+ of issues:

  • axe DevTools (Chrome extension) — automated scanner for common WCAG violations. Run on every page.
  • Keyboard-only navigation. Unplug your mouse. Tab through the entire page. Can you complete every task?
  • Screen reader test. macOS has VoiceOver built in (Cmd+F5). Windows has Narrator. iOS has VoiceOver. Spend 15 minutes navigating your own site with one of them.

Automated tools catch about 30% of accessibility issues. Keyboard testing catches another 30%. Screen reader testing catches the rest. None of them substitute for the others.

The shift

Accessibility isn't a separate engineering discipline. It's what good engineering looks like. The semantic element, the proper label, the visible focus ring — these are all just correct patterns that happen to be accessible. The teams that ship inaccessible products are usually shipping bad engineering for other reasons too. Fix the accessibility, and you tend to fix everything else along the way.