Technology · React
React Accessibility
Build accessible React apps using semantic HTML, ARIA attributes, and keyboard navigation.
TL;DR
- 01Use semantic HTML elements to give the browser meaningful structure.
- 02Add ARIA attributes only when semantic HTML is not enough.
- 03Test every interactive element with keyboard and screen readers.
Semantic HTML
- Use semantic elements instead of divs.
// Bad: divs with no meaning <div onClick={() => setOpen(!open)}>Menu</div> // Good: semantic button element <button onClick={() => setOpen(!open)}>Menu</button> - Use proper heading hierarchy to structure pages.
<h1>Main Title</h1> <section> <h2>Section Title</h2> <p>Content</p> </section> - Use landmark elements for page regions.
<header>Site Header</header> <nav>Navigation Links</nav> <main>Page Content</main> <footer>Site Footer</footer> - Use lists for groups of related items.
<ul> <li><a href="/about">About</a></li> <li><a href="/contact">Contact</a></li> </ul> - Use the button element for any clickable action.
// Button triggers an action — use <button> <button onClick={openModal}>Open Settings</button> // Link navigates — use <a> <a href="/profile">View Profile</a>
ARIA Attributes
- Use aria-label when the visible text doesn't describe the element.
<button aria-label="Close menu">×</button> <button aria-label="Delete item Alice">Delete</button> - Use aria-live to announce dynamic content to screen readers.
<div aria-live="polite" aria-atomic="true"> {statusMessage} </div> - Use role to define the purpose of a custom element.
<div role="button" onClick={handleClick} tabIndex="0"> Custom Button </div> - Use aria-expanded to indicate open or closed state.
<button aria-expanded={isOpen} aria-controls="menu-list" onClick={() => setIsOpen(!isOpen)} > Menu </button> - Use aria-describedby to link help text to an input.
<input id="password" type="password" aria-describedby="password-hint" /> <p id="password-hint">Must be at least 8 characters.</p>
Forms and Labels
- Link every input to a label using htmlFor and id.
<label htmlFor="email">Email:</label> <input id="email" type="email" /> - Use specific input types for built-in validation.
<input type="email" /> <input type="password" /> <input type="date" /> - Show validation errors with aria-invalid and aria-errormessage.
<input id="email" type="email" aria-invalid={!!error} aria-errormessage="email-error" /> {error && <p id="email-error" role="alert">{error}</p>} - Mark required fields with required and aria-required.
<label htmlFor="name">Name <span aria-hidden="true">*</span></label> <input id="name" type="text" required aria-required="true" /> - Use fieldset and legend to group related form controls.
<fieldset> <legend>Notification preferences</legend> <label><input type="checkbox" name="email" /> Email</label> <label><input type="checkbox" name="sms" /> SMS</label> </fieldset>
Keyboard Navigation
- Make interactive elements keyboard accessible by default.
// Buttons and links are keyboard accessible by default <button onClick={handleClick}>Click me</button> <a href="/next">Next page</a> - Use tabIndex="0" to make custom elements focusable.
<div role="button" tabIndex="0" onClick={handleClick} onKeyDown={(e) => e.key === "Enter" && handleClick()} > Custom Button </div> - Handle both Enter and Space for custom button elements.
function handleKeyDown(e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleClick(); } } - Trap focus inside modals when they are open.
function Modal({ isOpen, onClose, children }) { const firstFocusRef = useRef(null); useEffect(() => { if (isOpen) firstFocusRef.current?.focus(); }, [isOpen]); return isOpen ? ( <div role="dialog" aria-modal="true"> <button ref={firstFocusRef} onClick={onClose}>Close</button> {children} </div> ) : null; } - Use tabIndex="-1" to remove elements from tab order.
// Remove decorative icon from keyboard navigation <span tabIndex="-1" aria-hidden="true">★</span>
Screen Reader Testing
- Test with built-in screen readers on each platform.
macOS: VoiceOver (Cmd + F5) Windows: NVDA (free download at nvaccess.org) iOS: VoiceOver (Settings > Accessibility) Android: TalkBack (Settings > Accessibility) - Use aria-live for dynamic status messages.
<div aria-live="polite" aria-atomic="true"> {loadingMessage} </div> - Use role="alert" for urgent messages that need immediate attention.
{error && ( <div role="alert"> {error} </div> )} - Hide decorative elements from screen readers with aria-hidden.
<img src="decorative-bg.png" alt="" aria-hidden="true" /> <span aria-hidden="true">→</span> - Use jest-axe to automate accessibility checks in tests.
import { axe, toHaveNoViolations } from 'jest-axe'; expect.extend(toHaveNoViolations); test('has no accessibility violations', async () => { const { container } = render(<MyForm />); const results = await axe(container); expect(results).toHaveNoViolations(); });
Tip: Use semantic HTML first — it covers 80% of accessibility requirements without any extra ARIA attributes or JavaScript.
Warning: ARIA is not a substitute for semantic HTML — use it to enhance, not replace proper HTML structure.