Skip to content
Accessibility (a11y)

Cross-cutting Quality

Listen 0%
Speed

19 · Accessibility (a11y)

Building interfaces usable by everyone, including people using screen readers, keyboards, magnification, and other assistive technology: WCAG, semantic HTML, ARIA, keyboard/focus management, and how to test it. The discipline that also happens to make your code more robust and your tests more meaningful.


Positioning

Accessibility is a baseline of professional frontend work, not a feature: it’s a legal requirement in many markets (ADA/Section 508 in the US, the European Accessibility Act — enforceable since June 2025 across the EU, which directly affects anyone shipping to Europe — relevant to an Ireland-bound engineer), an ethical obligation, and — not incidentally — a forcing function for better engineering. Accessible markup is semantic markup, which is more robust, more testable (16’s role queries depend on it), and better for SEO. A senior engineer designs components accessible by default (which is exactly why headless primitives exist, 11).


Foundations

WCAG — the standard

Web Content Accessibility Guidelines organize success criteria under four principles — POUR:

  • Perceivable — content/UI must be perceivable (text alternatives, captions, sufficient color contrast, not relying on color alone).
  • Operable — fully usable by keyboard, enough time, no seizure triggers, clear navigation/focus.
  • Understandable — readable, predictable, with input assistance (labels, error messages).
  • Robust — works across user agents and assistive tech (valid, semantic markup, correct name/role/value).

Conformance levels: A (minimum) → AA (the practical/legal target) → AAA (aspirational). WCAG 2.2 (2023) is current and adds criteria like focus-appearance, dragging alternatives, and target size; WCAG 3.0 is in long-term draft. Aim for 2.2 AA.

The single most important rule: use semantic HTML

The platform gives you accessibility for free if you use the right elements. A real <button>, <a href>, <nav>, <main>, <h1–h6>, <label>, <table>, <ul> carry built-in role, keyboard behavior, focusability, and states that assistive tech understands. A <div onClick> has none of that and must reimplement all of it (and usually does so incompletely). The first rule of ARIA is: don’t use ARIA — use the native element.


Deep dive

1. The accessibility tree & the name/role/value model

The browser builds an accessibility tree (parallel to the DOM) that assistive tech reads. Every interactive element needs a computed name (label), role (what it is), and value/state (checked, expanded, selected, disabled). Semantic HTML populates this automatically; when you build custom widgets, you are responsible for getting name/role/value right.

2. ARIA — when and how

ARIA (Accessible Rich Internet Applications) adds roles, states, and properties for widgets the platform doesn’t have native elements for (tabs, comboboxes, tree views, live regions). Rules:

  • Don’t override native semantics (role="button" on a <button> is redundant/harmful).
  • Five rules of ARIA: prefer native HTML; don’t change native semantics; all interactive ARIA controls must be keyboard-operable; don’t role="presentation"/aria-hidden a focusable element; every interactive element needs an accessible name.
  • Key attributes: aria-label/aria-labelledby/aria-describedby (naming), aria-expanded/aria-selected/aria-checked (state), aria-live (announce dynamic changes), aria-hidden (hide decorative from AT), role.
  • “No ARIA is better than bad ARIA” — incorrect ARIA actively misleads users; the WebAIM Million survey consistently finds pages with ARIA average more detected errors because it’s misused.
  • Follow the ARIA Authoring Practices Guide (APG) for widget patterns — but prefer a tested primitive (11) over hand-rolling from the APG.

3. Keyboard accessibility & focus management

Everything operable by mouse must be operable by keyboard.

  • Logical tab order following DOM order; don’t use positive tabindex (use tabindex="0" to add to order, tabindex="-1" to make programmatically focusable but not tabbable).
  • Visible focus indicators — never outline: none without a replacement; WCAG 2.2 strengthens focus-appearance.
  • Focus management for dynamic UI: move focus into an opened modal, trap focus within it, restore focus to the trigger on close; manage focus on route changes (SPA navigation, 08) and after content insertion. This is exactly the hard, easy-to-get-wrong work that Radix/React Aria handle (11).
  • Skip links (“skip to main content”), keyboard shortcuts that don’t trap, and roving tabindex for composite widgets (menus, grids).
  • Support Escape, arrow keys, Enter/Space per the APG pattern for custom widgets.

4. Common content-level requirements

  • Images: meaningful alt; empty alt="" for decorative; describe complex images/charts.
  • Forms: every input has a programmatic <label> (not just placeholder); group with <fieldset>/<legend>; associate errors via aria-describedby and announce them; mark required state.
  • Color & contrast: ≥ 4.5:1 for normal text (3:1 large/UI components); never convey meaning by color alone (add icons/text).
  • Motion: respect prefers-reduced-motion; avoid flashing > 3×/sec.
  • Headings & landmarks: one logical heading outline; use landmark elements (<nav>, <main>, <aside>) for navigation by AT.
  • Live regions: announce async updates (toasts, validation, loading) with aria-live="polite"/assertive.
  • Zoom/reflow & target size: usable at 200–400% zoom; adequate touch-target size (WCAG 2.2).

5. SPA-specific concerns

  • Route changes don’t reload the page, so AT isn’t notified — manage focus (move to the new view’s heading) and optionally announce via a live region.
  • Dynamic content insertion must be discoverable (focus or live region).
  • Modals/overlays: <dialog> (native, 04) or a tested primitive gives focus trap + Escape + backdrop semantics; hand-rolled ones routinely fail.

6. Testing accessibility (16)

  • Automatedaxe-core (via @axe-core/react, Playwright/Vitest integrations, the axe DevTools extension), Lighthouse a11y audit, ESLint eslint-plugin-jsx-a11y, Storybook a11y addon. Catches ~30–50% of issues — necessary but not sufficient.
  • Manualkeyboard-only walkthrough (unplug the mouse), screen-reader testing (NVDA+Firefox, VoiceOver+Safari, JAWS), zoom/contrast checks, prefers-reduced-motion.
  • Testing Library (16) queries by role/name, so writing tests that way surfaces a11y gaps — if you can’t getByRole, neither can a screen reader.
  • Build a11y into the definition of done and CI (axe + jsx-a11y), not a pre-launch audit.

Worked example: an accessible disclosure (and why native wins)

// ❌ Inaccessible: no role, not focusable, no keyboard, no state exposed
<div onClick={() => setOpen(!open)}>Details</div>
{open && <div>{content}</div>}

// ✅ Native button carries role + focus + Enter/Space for free; ARIA exposes state
function Disclosure({ content }) {
  const [open, setOpen] = useState(false);
  const id = useId();
  return (
    <>
      <button aria-expanded={open} aria-controls={id} onClick={() => setOpen(o => !o)}>
        Details
      </button>
      <div id={id} hidden={!open}>{content}</div>
    </>
  );
}
// Screen reader announces "Details, button, collapsed/expanded"; keyboard works automatically.
// For anything more complex (menu, combobox, dialog) → use a Radix/React Aria primitive (11),
// which handles focus trapping, roving tabindex, and APG keyboard patterns correctly.

Pitfalls & gotchas

  • <div onClick> as a button — no role, focus, or keyboard; the most common a11y failure.
  • outline: none with no visible focus replacement — strands keyboard users.
  • Placeholder as label — disappears on input, unreadable by some AT; use a real <label>.
  • Bad/excessive ARIA — misused role/aria-* misleads more than no ARIA.
  • Color-only meaning (red = error with no text/icon) and low contrast.
  • Unmanaged focus on modals and SPA route changes.
  • Auto-playing motion ignoring prefers-reduced-motion.
  • “We’ll add a11y later” — retrofitting is far costlier than building it in; bake it into the DS (11).
  • Relying on automated tests alone — they miss most issues; keyboard + screen-reader testing is required.

Interview questions

  1. What are WCAG’s four principles (POUR) and the conformance levels? Which do you target?
  2. Why is semantic HTML the foundation of accessibility?
  3. What is the accessibility tree and the name/role/value model?
  4. When should you use ARIA — and what’s the first rule of ARIA?
  5. Why can “bad ARIA be worse than no ARIA”?
  6. How do you manage focus for a modal and for SPA route changes?
  7. What’s wrong with tabindex positive values and outline: none?
  8. How do you make a form accessible?
  9. What can automated a11y testing catch, and what must you test manually?
  10. How does accessible markup relate to testability (Testing Library) and SEO?

Recommendations

  • Target WCAG 2.2 AA; treat a11y as definition-of-done, enforced in CI (axe + eslint-plugin-jsx-a11y), not a late audit.
  • Semantic HTML first; reach for ARIA only for widgets with no native element, following the APG.
  • Build interactive components on Radix/React Aria primitives (11) so focus trapping and keyboard patterns are correct by default.
  • Manage focus explicitly for modals, menus, and SPA navigation; keep focus visible.
  • Real <label>s, ≥ 4.5:1 contrast, no color-only meaning, respect prefers-reduced-motion.
  • Test with the keyboard and a screen reader, not just automated tools.
  • Write tests with role/name queries so they double as a11y checks (16).

Books & references

  • WCAG 2.2 (w3.org/TR/WCAG22) and the ARIA Authoring Practices Guide (w3.org/WAI/ARIA/apg) — the primary, authoritative sources (with keyboard patterns for every widget).
  • “Inclusive Components” — Heydon Pickering (inclusive-components.design). The best practical book/site for building accessible component patterns. Top community pick.
  • MDN Accessibility docs — approachable, current references on ARIA, semantics, and testing.
  • WebAIM (webaim.org) — articles, the contrast checker, screen-reader guides, and the annual “WebAIM Million” survey.
  • The A11Y Project (a11yproject.com) — checklists and practical patterns.
  • Deque University / axe (dequeuniversity.com) — testing tooling and structured courses.
  • “A Web for Everyone” — Horton & Quesenbery; “Accessibility for Everyone” — Laura Kalbag (A Book Apart) — the strategy/culture side.

Connections

Frontend Deep-Dive Library · content is the single source of truth.