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-hiddena 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(usetabindex="0"to add to order,tabindex="-1"to make programmatically focusable but not tabbable). - Visible focus indicators — never
outline: nonewithout 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; emptyalt=""for decorative; describe complex images/charts. - Forms: every input has a programmatic
<label>(not just placeholder); group with<fieldset>/<legend>; associate errors viaaria-describedbyand 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)
- Automated — axe-core (via
@axe-core/react, Playwright/Vitest integrations, the axe DevTools extension), Lighthouse a11y audit, ESLinteslint-plugin-jsx-a11y, Storybook a11y addon. Catches ~30–50% of issues — necessary but not sufficient. - Manual — keyboard-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’tgetByRole, 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: nonewith 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
- What are WCAG’s four principles (POUR) and the conformance levels? Which do you target?
- Why is semantic HTML the foundation of accessibility?
- What is the accessibility tree and the name/role/value model?
- When should you use ARIA — and what’s the first rule of ARIA?
- Why can “bad ARIA be worse than no ARIA”?
- How do you manage focus for a modal and for SPA route changes?
- What’s wrong with
tabindexpositive values andoutline: none? - How do you make a form accessible?
- What can automated a11y testing catch, and what must you test manually?
- 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, respectprefers-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
11-design-systems.md— headless primitives (Radix/React Aria) exist to bake a11y in; the DS is how a11y scales across teams.16-testing.md— Testing Library role queries surface a11y gaps; axe + jsx-a11y in CI.04-the-web-platform.md—<dialog>, semantic elements, focus APIs, the accessibility tree.05-react-internals-and-patterns.md— focus management,useId, and component patterns for accessible widgets.08-nextjs-and-meta-frameworks.md— SPA route-change focus management.15-performance-and-core-web-vitals.md— reduced motion, and the overlap of usability and performance.