React Accessibility: Building WCAG-Compliant React Apps
React Accessibility: Building WCAG-Compliant React Apps
React is the dominant JavaScript framework for building web applications in 2026. It's powerful, flexible, and has excellent ecosystem support — but none of that makes your app accessible automatically. Accessibility in React requires deliberate choices at every level of your component hierarchy.
This guide covers the essential patterns, common pitfalls, and practical techniques for building React apps that conform to WCAG 2.1 Level AA.
React's Accessibility Foundations
React itself is accessibility-neutral. JSX renders to HTML, and HTML has accessibility semantics built in through native elements. The problem arises when developers:
- Use
<div>and<span>instead of semantic HTML elements - Build custom components without proper ARIA attributes
- Manage focus incorrectly during state transitions
- Forget to handle keyboard events alongside mouse events
- Create dynamic content that isn't announced to screen readers
Essential Principle: Use Semantic HTML First
Before reaching for ARIA, use the correct HTML element. React JSX supports the full HTML element set.
// Bad: div as button
<div onClick={handleClick} className="btn">Submit</div>
// Good: actual button element
<button onClick={handleClick} className="btn">Submit</button>
Native HTML elements like <button>, <a>, <input>, and heading elements (<h1>–<h6>) come with built-in accessibility semantics, keyboard support, and screen reader compatibility. Custom components built from <div> require you to recreate all of that manually.
Accessible React Component Patterns
Form Inputs with Labels
// Always associate labels with inputs
<div>
<label htmlFor="email">Email address</label>
<input
type="email"
id="email"
name="email"
aria-required="true"
aria-describedby={error ? "email-error" : undefined}
/>
{error && (
<span id="email-error" role="alert">{error}</span>
)}
</div>
Modal Dialogs
Modals are one of the most common accessibility failure points in React apps. A properly accessible modal must:
- Move focus into the modal when it opens
- Trap focus within the modal while it's open
- Return focus to the triggering element when it closes
- Have a proper role="dialog" and aria-modal="true"
- Have an accessible name via aria-labelledby
- Be closeable via the Escape key
Consider using Radix UI Primitives or Headless UI for complex components like modals — these libraries handle accessibility correctly out of the box.
Focus Management After Navigation
// In a React Router app, announce page changes to screen readers
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
function PageAnnouncer() {
const location = useLocation();
const announcerRef = useRef(null);
useEffect(() => {
if (announcerRef.current) {
announcerRef.current.textContent = document.title;
}
}, [location]);
return (
<div
ref={announcerRef}
aria-live="polite"
aria-atomic="true"
className="sr-only"
/>
);
}
Live Regions for Dynamic Content
// Announce dynamic content updates to screen readers
function StatusMessage({ message, type }) {
return (
<div
role="status"
aria-live={type === 'error' ? 'assertive' : 'polite'}
aria-atomic="true"
>
{message}
</div>
);
}
Common React Accessibility Mistakes
1. Event Handlers Without Keyboard Support
// Bad: click only
<div onClick={handleSelect}>Option 1</div>
// Good: keyboard + click
<div
onClick={handleSelect}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleSelect(); }}
role="option"
tabIndex={0}
aria-selected={isSelected}
>Option 1</div>
2. Missing Image Alt Text
// Bad
<img src={product.image} />
// Good
<img src={product.image} alt={product.name} />
// Decorative image
<img src={decorativeIcon} alt="" aria-hidden="true" />
3. Insufficient Color Contrast
React doesn't help you with color contrast — that's a CSS problem. Ensure all text has at least 4.5:1 contrast against its background (3:1 for large text, 18px+ regular or 14px+ bold).
Testing React Accessibility
Use a multi-pronged testing approach:
- jest-axe — Integrates axe-core into your Jest test suite for automated component-level testing
- @testing-library/react — Encourages accessible query patterns (getByRole, getByLabelText)
- Storybook + a11y addon — Tests components in isolation
- EAAPass — Tests your deployed application against all 50 WCAG 2.1 AA criteria and provides a compliance report
Recommended Libraries
- Radix UI — Unstyled, accessible primitives for complex components
- Headless UI — Accessible component implementations from Tailwind Labs
- React Aria (Adobe) — Full accessibility hooks library
- focus-trap-react — Focus trapping for modals
Audit Your React App with EAAPass
No matter how careful you are with individual components, you need to audit the deployed application to catch integration-level issues. EAAPass renders your React app with a full headless browser, executes JavaScript, and then tests the rendered output against WCAG 2.1 AA. You'll catch issues that component-level testing misses.
Run your free audit at eaapass.eu — no signup, results in 60 seconds.