React's component-based architecture makes it powerful for building complex UIs, but it also makes it easy to accidentally create inaccessible interfaces. This guide covers essential patterns for building accessible React applications.
Semantic HTML First
Before reaching for ARIA, use semantic HTML elements. They have built-in accessibility features.
// Bad: div with click handler
<div onClick={handleClick}>Submit</div>
// Good: native button
<button onClick={handleClick}>Submit</button>
Native elements provide:
- Keyboard accessibility (Enter/Space to activate buttons)
- Proper role announcements to screen readers
- Focus management
Managing Focus
Single-page applications often fail to manage focus when content changes. When navigating to a new "page," focus should move to the new content.
import { useRef, useEffect } from 'react';
function PageContent({ title, children }) {
const headingRef = useRef(null);
useEffect(() => {
// Focus the heading when page loads
headingRef.current?.focus();
}, []);
return (
<main>
<h1 ref={headingRef} tabIndex={-1}>{title}</h1>
{children}
</main>
);
}
Accessible Forms
Every form input needs an associated label.
function TextField({ label, id, ...props }) {
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} {...props} />
</div>
);
}
Error Messages
function TextField({ label, id, error, ...props }) {
const errorId = `${id}-error`;
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
aria-invalid={error ? 'true' : undefined}
aria-describedby={error ? errorId : undefined}
{...props}
/>
{error && (
<span id={errorId} role="alert">
{error}
</span>
)}
</div>
);
}
Live Regions for Dynamic Content
When content updates dynamically, screen readers need to be notified.
function SearchResults({ results, isLoading }) {
return (
<div>
<div aria-live="polite" aria-busy={isLoading}>
{isLoading && <span>Loading results...</span>}
</div>
<div aria-live="polite">
{!isLoading && (
<span>{results.length} results found</span>
)}
</div>
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}
Testing React Accessibility
Use these tools to catch issues during development:
- eslint-plugin-jsx-a11y: Catches common issues at build time
- React DevTools: Inspect component props and state
- axe DevTools: Browser extension for runtime testing
- SiteDNA: Full-site scanning to catch issues across all pages
Key Takeaways
- Use semantic HTML before ARIA
- Manage focus when content changes
- Associate labels with all form inputs
- Trap focus in modals and restore it on close
- Use live regions for dynamic updates
- Test with keyboard navigation and screen readers