Accessible React apps start with semantic HTML elements, use ARIA attributes only when native semantics fall short, manage focus programmatically for dynamic content, and ensure full keyboard navigation — interviewers test whether you treat a11y as an afterthought or a first-class concern.
Use native elements (<button>, <nav>, <main>, <h1>–<h6>) for built-in accessibility — a <div onClick> lacks focus, keyboard activation, and ARIA role.
ARIA attributes (aria-label, aria-live, aria-expanded) fill gaps when no native HTML element provides the needed semantics — don't use ARIA to duplicate what HTML already does.
Use useRef + focus() to move focus when dynamic content appears (modals, route changes, error messages) — the browser doesn't manage focus for React-rendered content automatically.
Every interactive element must work with keyboard alone — native elements are accessible by default, custom widgets need WAI-ARIA patterns and visible focus indicators.
eslint-plugin-jsx-a11y and axe-core catch ~30-40% of issues at lint/dev time — manual keyboard testing and screen reader testing catch the rest.
Accessibility (a11y) in React means ensuring your application is usable by everyone, including people who use screen readers, keyboard navigation, voice control, or other assistive technologies. The principles are the same as general web accessibility, but React's component model introduces specific patterns and pitfalls.
The most impactful accessibility improvement is using the right HTML elements. <button> already has click handling, focus management, Enter/Space activation, and an implicit button ARIA role. A <div onClick> has none of these — you'd need to add role="button", tabIndex={0}, onKeyDown for Enter and Space, and still wouldn't get native form submission.
Use <nav>, <main>, <header>, <footer>, <section>, <article>, <aside> to create landmarks that screen reader users navigate between. Use <h1>–<h6> in order (no skipping levels) to create a navigable document outline.
ARIA (Accessible Rich Internet Applications) attributes are fully supported in React with the same names as in HTML (aria-label, aria-describedby, aria-expanded, aria-hidden, etc.) — no camelCase conversion needed. The first rule of ARIA is: don't use ARIA if a native HTML element or attribute provides the semantics you need. ARIA should fill gaps, not duplicate what HTML already provides.
Common ARIA patterns in React:
aria-label for icon-only buttons: <button aria-label="Close"><XIcon /></button>aria-live="polite" for dynamic content announcements (toast notifications, loading states)aria-expanded and aria-controls for disclosure widgets (dropdowns, accordions)role="alert" for error messages that need immediate announcementReact's dynamic rendering creates focus challenges that static HTML doesn't have. When content appears or disappears (modals, route changes, dynamic forms), the browser's focus can be lost or left in an illogical position.
Use useRef + focus() to programmatically move focus:
tabIndex={-1})Focus trapping (keeping Tab within a modal) requires intercepting Tab/Shift+Tab keydown events. Libraries like focus-trap-react handle this reliably.
Every interactive element must be reachable and operable via keyboard. Native HTML elements (<button>, <a>, <input>, <select>) are keyboard-accessible by default. Custom widgets built with <div> or <span> are not.
For custom components, implement the WAI-ARIA Authoring Practices patterns — for example, a custom dropdown needs Arrow Up/Down to move between options, Enter/Space to select, Escape to close, and Home/End to jump to first/last.
Visible focus indicators are required — never use outline: none without providing an alternative focus style. The :focus-visible pseudo-class shows focus rings only for keyboard users, not mouse clicks.
Automated tools catch ~30-40% of accessibility issues:
eslint-plugin-jsx-a11y catches common JSX mistakes (missing alt text, invalid ARIA, click handlers on non-interactive elements) at lint time@axe-core/react logs accessibility violations to the dev console during developmentjest-axe runs axe-core checks in unit tests: expect(await axe(container)).toHaveNoViolations()The remaining 60-70% requires manual testing: keyboard navigation flow, screen reader behavior (VoiceOver, NVDA, JAWS), color contrast, and real-user testing.
Accessibility is not a feature to add later — it's a quality of well-written HTML and components. Semantic HTML handles 80% of a11y. ARIA fills gaps for custom widgets. Focus management handles React's dynamic rendering. Keyboard navigation ensures every interaction works without a mouse. The eslint-plugin-jsx-a11y plugin catches common mistakes, but manual and screen reader testing are essential for real coverage.
Fun Fact
The eslint-plugin-jsx-a11y rule that catches the most violations is 'click-events-have-key-events' — developers put onClick on <div> elements so frequently that this single rule accounts for more warnings than any other accessibility lint check across React codebases.