Back to Accessibility
Accessibility
hard
senior

How would you implement accessibility at scale across a large React application?

Build a11y into the design system, not into individual screens. Use semantic HTML + accessible primitives (Radix, React Aria), enforce with linting (eslint-plugin-jsx-a11y) and automated audits (axe in CI + Lighthouse), test with keyboard + a real screen reader (NVDA/VoiceOver), and own metrics like keyboard coverage and contrast pass rate. Treat a11y like a feature with a budget and a tracking dashboard, not a pre-launch checklist.

9 min read·~20 min to think through

Accessibility at scale is an architecture problem, not a per-component problem. You can't tag a thousand engineers to remember ARIA — you set up the system so the easy path is the accessible path.

The four layers of an a11y program.

1. Primitives layer — make the right thing easy.

Build (or adopt) an accessible component library: Button, Dialog, Menu, Combobox, Tabs, Tooltip. Each one bakes in:

  • Correct semantic element (<button>, not <div role="button">).
  • Focus management (trap focus in modal, return focus on close).
  • Keyboard support per WAI-ARIA Authoring Practices.
  • ARIA states (aria-expanded, aria-selected, aria-controls).
  • Live regions for dynamic content (aria-live="polite").

Don't build these from scratch. Use:

  • Radix UI — headless primitives, complete ARIA + keyboard impl.
  • React Aria (Adobe) — hooks that give you accessibility without prescribing markup.
  • Headless UI — Tailwind-flavored.

Adopt one; ban handrolled modals/menus/dropdowns in code review.

2. Enforcement layer — catch regressions automatically.

  • eslint-plugin-jsx-a11y flags missing alt, click handlers on non-interactive elements, etc. Make it error-level.
  • axe-core integrated via @axe-core/react in dev mode, and jest-axe in component tests:

``tsx expect(await axe(container)).toHaveNoViolations(); ``

  • Playwright + @axe-core/playwright for E2E a11y on key flows.
  • Storybook a11y addon — runs axe per story, surfaces violations to designers.
  • CI gate — block merge on new a11y errors. Allow-list existing violations with a tracked debt issue, not silent suppression.

3. Testing layer — humans, not bots.

Automation catches ~30% of issues. The rest needs:

  • Keyboard-only walk-through of each major flow. Every interaction reachable, focus visible, no traps.
  • Screen reader (NVDA on Windows, VoiceOver on Mac/iOS, TalkBack on Android) — sample a handful of pages per release. Look for announcement order, missing labels, redundant info.
  • Zoom + reduced-motion — text scales to 200%, animations respect prefers-reduced-motion.
  • Contrast — design system enforces tokens; design reviews check derived states (hover, disabled).

4. Culture & metrics layer.

  • Dashboard: % of pages passing axe with zero violations, contrast pass rate by token, % of components keyboard-tested. Make it visible.
  • Per-team budgets: a violation per page max, owned by the team shipping the page.
  • Accessibility champions — one per pod, reviewer-of-record for a11y PRs.
  • Training: 1-hour onboarding (keyboard + screen reader basics), recurring office hours.

Concrete patterns you'll repeat.

  • Skip link<a href="#main" class="sr-only-focusable">Skip to content</a>.
  • Focus management on route change — move focus to <h1> of new page so SR users hear it.
  • Modals — trap focus inside, return focus on close, ESC closes, click-outside closes (but not for destructive actions).
  • Forms — every <input> has a programmatic label (visible <label> or aria-labelledby); errors associated via aria-describedby; field-level + summary error patterns.
  • Live regionsrole="status" for toasts, role="alert" for errors, aria-live="polite" for non-urgent updates.
  • Icon buttonsaria-label or visually-hidden label; icon-only buttons without one are invisible to SR users.
  • Dynamic content — when content updates without a route change (search results, data tables), announce via live region.

The bait questions interviewers love.

  • "Why prefer <button> over <div onClick>?" — Native button is focusable, has Space/Enter activation, has role, has disabled semantics, is form-submittable. A div has none.
  • "How do you make a custom dropdown accessible?" — Radix/React Aria, or implement WAI-ARIA combobox spec (10+ keyboard interactions). Don't pretend to remember all of them.
  • "What's the difference between aria-hidden and role=presentation?" — aria-hidden removes the subtree from the accessibility tree entirely (don't use on focusable elements). role=presentation strips the semantic role of the element but children remain.
  • "When do you need aria-live?" — for content that changes without user navigation and that matters to know about: form errors, status messages, async results.

Senior framing. The interviewer is checking whether the candidate has shipped a11y at scale, or just learned a checklist. The proof points: design system ownership, automated gates in CI, recurring screen-reader testing, and a dashboard. The candidate who says "I always add aria-label to my buttons" is junior. The candidate who says "We made our Button primitive enforce a label at the type level, and our axe CI gate has been zero-violation for 18 months" is senior.

Follow-up questions

  • Why prefer Radix / React Aria over hand-rolled primitives?
  • What does axe catch and what does it miss?
  • How do you announce dynamic content to screen readers?
  • Difference between aria-hidden, role=presentation, and visually-hidden CSS?

Common mistakes

  • Using `<div onClick>` instead of `<button>`.
  • Trapping focus in a modal but never returning focus on close.
  • Toasts that don't use a live region — invisible to screen readers.
  • Relying solely on automated tests; never touching a real screen reader.

Performance considerations

  • axe is fast in dev, but full-page audits in CI can add minutes — scope to changed routes.
  • Live regions don't cause re-renders, but excessive announcements (every key press) overwhelm SR users.

Edge cases

  • Single-page-app route change — must move focus + announce, otherwise SR users get no signal.
  • Forms with conditional fields — labels and error wiring must stay consistent as the DOM changes.
  • Drag-and-drop — provide a keyboard alternative (move up/down with arrows).

Real-world examples

  • GitHub, Atlassian, and Adobe ship dedicated a11y teams + dashboards.
  • Reach UI / Radix originated as solutions for this scaling problem.

Related questions