Accessibility

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.

5 min read Level 2/5 #react#accessibility#a11y
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>

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 →