// guide

React Accessibility: Building Inclusive Components

React does not make your app inaccessible on its own — but its component model, JSX quirks, and client-side rendering create a predictable set of accessibility pitfalls. This hub is the map: JSX attribute differences, keeping markup semantic, managing focus and announcements, portals for overlays, and the React a11y toolchain, with deep-dive guides on focus and forms.

intermediate react

// 01 · does react help or hurt accessibility?

Does React Help or Hurt Accessibility?

Neither, inherently. React renders whatever markup you tell it to, so a React app is exactly as accessible as the code you write — it can be flawless or unusable. What React changes is which mistakes are easy to make. Its component model and client-side rendering introduce a specific, repeatable set of traps, and once you know them they are straightforward to avoid.

Three themes cause most React accessibility bugs, and this hub is organized around them:

  • Non-semantic markup — reaching for <div> and onClick instead of real elements.
  • Lost focus — DOM swapping in and out without managing where focus goes. Covered in depth in focus management in React.
  • Silent updates — content changing without a page load and without announcing itself.
The requirements do not change Accessibility rules are about the rendered DOM, not the framework that produced it. Every component pattern on this site — its keyboard behavior and ARIA — applies identically in React. React is just the delivery mechanism.

// 02 · jsx attribute differences

JSX Attribute Differences

JSX looks like HTML but renames a couple of attributes, and those renames are a common source of accessibility bugs — especially forgetting htmlFor, which silently breaks label association.

HTML attributes in JSX
// Renamed in JSX:
<label htmlFor="email">Email</label>   // not for=
<input id="email" className="field" /> // not class=

// Kept exactly as in HTML — hyphens and all:
<button
  aria-expanded={isOpen}
  aria-controls="menu-list"
  role="button"
>
  Menu
</button>

The key facts: class becomes className and for becomes htmlFor, but aria-*, data-*, and role are written exactly as in HTML — they keep their hyphens and are not camelCased. Event props like onClick are camelCased, but that is a syntax change, not a licence to put click handlers on the wrong element.

// 03 · reach for semantic html, not divs

Reach for Semantic HTML, Not Divs

Because everything in JSX is "just a component," it is tempting to build every control out of <div> and wire up an onClick. That produces something that looks right and is completely inaccessible: not focusable, not keyboard-operable, and announced as nothing.

Use the real element
// Avoid: not focusable, no keyboard, no role
<div className="btn" onClick={save}>Save</div>

// Prefer: focus, Enter/Space, and "button" role for free
<button type="button" onClick={save}>Save</button>

// Navigation is a link, not an onClick div
<a href="/settings">Settings</a>

A native <button> is focusable, responds to Enter and Space, and is announced as a button — none of which a div gives you without a pile of extra ARIA and key handlers. Reach for the element that matches the job: <button> for actions, <a href> for navigation, <nav>/<main>/<header> for structure. See the semantic HTML guide and the accessible links guide for the full picture. React Fragments (<>...</>) help here too — they let you return multiple elements without wrapping everything in a meaningless <div>.

Let the linter catch these eslint-plugin-jsx-a11y flags most of these mistakes — a click handler on a non-interactive element, a missing alt, an invalid ARIA prop — right in your editor. Install it early so the whole team gets the feedback before code review.

// 04 · managing focus

Managing Focus

This is the single biggest React-specific accessibility concern. When React swaps DOM in and out — a route change, a modal opening, a list item removed — the focused element can disappear, and the browser drops focus to the top of the page or nowhere. React does not manage this for you.

The core pattern is a ref plus a useEffect, so focus moves after React commits the DOM:

Move focus to a heading on mount
function Dashboard() {
  const headingRef = useRef(null);

  useEffect(() => {
    headingRef.current?.focus();
  }, []);

  // tabIndex={-1} lets a non-interactive element take programmatic focus
  return <h1 tabIndex={-1} ref={headingRef}>Dashboard</h1>;
}

That is the foundation; the full set of patterns — focus on client-side route changes, trapping focus in modals, and restoring it on close — is covered in the dedicated focus management in React guide, which builds directly on the general focus management guide.

// 05 · announcing dynamic updates

Announcing Dynamic Updates

React makes it trivial to change part of the page in response to state — a search result count, a "saved" confirmation, an error. A sighted user sees it; a screen reader user hears nothing, because no page navigation occurred. The fix is an ARIA live region.

A live region for status messages
function SaveStatus({ message }) {
  // The region must be in the DOM before the text changes,
  // so render it always and update its contents.
  return (
    <p role="status" aria-live="polite">
      {message}
    </p>
  );
}

The critical detail: the live region has to be present in the DOM before its contents change. If you conditionally render the whole element only when a message exists, many screen readers will not announce it, because the region did not exist to be watched. Render the container unconditionally and update the text inside. For urgent messages use role="alert" (assertive); for everything else, aria-live="polite". The live regions guide covers the nuances.

// 06 · portals for overlays

Portals for Overlays

Modals, popovers, and toasts often need to escape a parent with overflow: hidden or a stacking context. React's createPortal renders them elsewhere in the DOM — typically at the end of <body> — while keeping them in the React tree.

A portal-based dialog
import { createPortal } from 'react-dom';

function Modal({ children, onClose }) {
  return createPortal(
    <div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
      {children}
    </div>,
    document.body
  );
}

A portal solves the visual stacking problem, but not the accessibility work: an accessible dialog still needs role="dialog", aria-modal="true", a label, focus moved in on open, focus trapped while open, Escape to close, and focus returned to the trigger on close. Those behaviors are the same as any dialog — follow the modal pattern, and see focus management in React for the focus half.

// 07 · the react a11y toolchain

The React A11y Toolchain

Three complementary layers catch different classes of problem, and they all wrap the same axe-core engine used across the rest of the ecosystem.

Tool When it runs Catches
eslint-plugin-jsx-a11y As you type / on lint Static JSX mistakes — bad roles, missing alt, click on a non-interactive element
@axe-core/react At runtime in dev Rendered-DOM issues, logged to the console as components mount
jest-axe / @axe-core/playwright In your test suite Per-component and full-flow violations, including interactive states

Use all three: the linter for instant feedback, @axe-core/react to surface runtime issues while you develop, and axe in your tests to stop regressions. None of them replaces manual keyboard and screen reader testing — automated tools catch only the deterministic subset. For how these fit the wider picture, see the accessibility testing tools comparison.

Keep going This hub is the overview. Go deeper with focus management in React and accessible forms in React, and use the React checklist as a pre-ship pass. For apps that also do client-side routing, pair it with the SPA accessibility guide.

Frequently asked questions

Is React bad for accessibility?

No — React is accessibility-neutral. It renders whatever markup you write, so a React app can be fully accessible or completely broken depending on the code. What React does is make certain mistakes easy: reaching for <div onClick> instead of a button, losing focus when views swap out, and forgetting to announce content that appears without a page load. Learn the handful of React-specific patterns and those problems disappear.

How is JSX different from HTML for accessibility?

A few attributes are renamed: class becomes className and for becomes htmlFor (forgetting the latter is a common label bug). Most others carry over unchanged — importantly, aria-* and data-* attributes keep their hyphens and are written exactly as in HTML, as is role. Event handlers are camelCased (onClick), but that does not change the accessibility rules — an onClick still belongs on a <button>, not a <div>.

Why does focus get lost in React apps?

Because React swaps DOM in and out without a page reload. When a route changes, a modal opens, or a list item is removed, the element that had focus can vanish — and the browser dumps focus back to the top of the page or nowhere useful. React gives you no automatic focus management, so you handle it explicitly with refs and useEffect. See the focus management in React guide for the patterns.

What tools check accessibility in a React project?

Three layers, all built on the same engines. eslint-plugin-jsx-a11y catches issues statically in your JSX as you type. @axe-core/react audits the rendered DOM at runtime and logs findings to the console during development. And jest-axe plus @axe-core/playwright assert accessibility in your unit and end-to-end tests. See the accessibility testing tools comparison for how they fit together.

Do I need a component library for accessible React?

Not necessarily, but a good headless library (one that ships behavior and ARIA without styling) saves you from re-implementing hard widgets like comboboxes, dialogs, and menus correctly. If you build your own, base each component on the corresponding accessible pattern — the keyboard interactions and ARIA are the same whether the markup comes from React or plain HTML. React is just the delivery mechanism; the accessibility requirements do not change.