Accessibility is not an afterthought — it's a core part of building great software. In this post, we'll walk through the practical steps to make your React components work for everyone, including people who rely on screen readers, keyboard navigation, or other assistive technologies.
Why Accessibility Matters
Over 1 billion people worldwide live with some form of disability. When we build inaccessible interfaces, we're actively excluding a significant portion of our potential users. Beyond the ethical argument, accessibility also improves SEO, usability for all users, and in many regions is a legal requirement.
The Basics: Semantic HTML First
The single most impactful thing you can do is use the right HTML element for the job. A <button> is not a <div>. A <nav> is not a <div>. Semantic elements come with built-in accessibility behaviours — keyboard focus, ARIA roles, and screen reader announcements — for free.
// ❌ Bad — a div pretending to be a button
<div onClick={handleClick}>Submit</div>
// ✅ Good — a real button with all the right behaviours
<button type="button" onClick={handleClick}>Submit</button>
ARIA Roles and Attributes
When you genuinely can't use a semantic element, ARIA (Accessible Rich Internet Applications) attributes let you communicate intent to assistive technologies. The golden rule: no ARIA is better than bad ARIA.
Common patterns you'll use in React:
aria-label— provides a text label when visible text isn't availablearia-expanded— communicates open/closed state of dropdowns and accordionsaria-live— announces dynamic content changes to screen readersaria-describedby— links an element to a longer descriptionrole="dialog"— marks modal dialogs so screen readers handle focus correctly
Keyboard Navigation
Every interactive element must be reachable and operable via keyboard alone. This means:
- All interactive elements must be focusable (use
tabIndex={0}only when necessary) - Focus order must follow a logical reading order
- Custom components must handle Enter, Space, and arrow keys where appropriate
- Modal dialogs must trap focus while open and restore it on close
function AccessibleModal({ isOpen, onClose, children }) {
const closeRef = useRef(null)
// Move focus to close button when modal opens
useEffect(() => {
if (isOpen) closeRef.current?.focus()
}, [isOpen])
if (!isOpen) return null
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<button ref={closeRef} onClick={onClose} aria-label="Close dialog">
✕
</button>
{children}
</div>
)
}
Focus Management in React
React's declarative model can make focus management tricky. When content changes dynamically — a modal opens, a route changes, a form error appears — you need to manually move focus to the right place.
Use useRef to hold a reference to the element that should receive focus, and call .focus() inside a useEffect that runs when the relevant state changes.
Testing Your Work
Don't rely solely on automated tools. They catch roughly 30% of accessibility issues. Here's a practical testing checklist:
- Navigate your entire page using only the Tab key
- Test with VoiceOver (macOS/iOS) or NVDA (Windows)
- Run axe DevTools or Lighthouse in Chrome DevTools
- Check colour contrast ratios with the WebAIM Contrast Checker
- Zoom to 200% and verify nothing breaks
Wrapping Up
Accessibility is a skill that compounds. Start with semantic HTML, layer in ARIA only when needed, ensure keyboard operability, and test with real assistive technologies. Your users — all of them — will thank you.