Make Sure Everyone Can Use What You Built
Accessibility
A handful of practices catch most accessibility issues in React apps — semantic HTML, focus management, ARIA in the right spots.
What you'll learn
- Use semantic HTML by default
- Label every form input
- Manage focus in modals and route changes
Accessibility isn’t a separate phase — it’s how you build. A handful of habits cover 80% of the issues.
1. Semantic HTML First
The single most impactful thing. A <button> is keyboard-focusable
and announces “button” to screen readers automatically. A
<div onClick> is none of that.
// ✗
<div onClick={save}>Save</div>
// ✓
<button onClick={save}>Save</button> Use the right tag — <nav>, <main>, <header>, <footer>,
<article>, <aside>, <button>, <a href>. Don’t reach for a
<div> when the semantic element exists.
2. Label Every Input
Every form input needs an accessible name. The cleanest way:
<label htmlFor>.
const id = useId();
<label htmlFor={id}>Email</label>
<input id={id} type="email" /> Or wrap the input inside the label:
<label>
Email
<input type="email" />
</label> If a visible label isn’t possible, use aria-label:
<button aria-label="Close" onClick={close}>×</button> 3. Use Real Links for Navigation
A “link” that’s really a button confuses keyboard and screen-reader users:
// ✗ Looks like a link, isn't one
<span onClick={() => navigate("/about")}>About</span>
// ✓
<Link to="/about">About</Link> Buttons trigger actions; links navigate.
4. Focus Management
Two patterns:
On route change
When the user navigates, focus the heading or main region so screen readers announce the new content:
useEffect(() => {
document.getElementById("main")?.focus();
}, [pathname]); In modals
Move focus into the modal when it opens, and back to the trigger when it closes:
function Modal({ open, onClose, children }) {
const ref = useRef(null);
useEffect(() => {
if (open) ref.current?.focus();
}, [open]);
return open ? (
<div role="dialog" aria-modal="true" tabIndex={-1} ref={ref}>
{children}
<button onClick={onClose}>Close</button>
</div>
) : null;
} 5. Visible Focus
Never outline: none on focusable elements unless you replace it
with something visible. Keyboard users can’t see where they are
without focus rings.
6. Color Contrast
Text needs to have enough contrast against its background. Quick check: open Chrome DevTools, hover any text color in the sidebar — it shows the contrast ratio. WCAG AA needs 4.5:1 for body text.
7. Test With a Keyboard
Tab through your page. Can you reach everything? Can you activate buttons with Enter/Space, links with Enter? If something is unreachable or the focus ring is invisible, fix it.
Tools
- axe DevTools (browser extension) — runs automated checks
- Lighthouse (Chrome DevTools) — accessibility audit included
- VoiceOver (Mac) / NVDA (Windows) — try your app with a screen reader, even briefly
Automated tools catch maybe 30% of issues. Keyboard-only testing catches another 30%. The rest needs human judgment.
Up Next
Last stop — shipping your app to the internet.
Deployment →