React Accessibility (a11y): Build Apps Everyone Can Use

React.js Full Course: Beginner to Advanced · Advanced React · 8 min read · Updated

Everyone can use it

  1. Start with the right HTML elements. A real button works with keyboard and screen reader for free.
  2. Add a label to every field and alt text to every meaningful image.
  3. Make everything work with the keyboard only, and keep the focus ring visible.
  4. Check contrast, do not use colour alone, and announce changes with live regions.
  5. Test with only the keyboard, Lighthouse or axe, and a screen reader. Tools find only part of the problems.

Accessibility, often written a11y, means making your app usable by everyone, including people who are blind, have low vision, cannot use a mouse, are deaf, or have trouble reading or focusing. It also helps people on a bright street, with one hand free, or with a slow connection. In many countries it is also a legal requirement for some kinds of sites. The good news: most of it is simple, and React makes it as easy as plain HTML.

Who benefits?

  • Screen reader users hear the page read aloud. They depend on correct structure and labels.
  • Keyboard users move with Tab, Enter, Space and the arrow keys, with no mouse.
  • People with low vision zoom the page, use large text, or need high colour contrast.
  • People with colour blindness cannot rely on colour alone.
  • People with hearing loss need captions for video and audio.
  • People with attention or reading difficulties benefit from clear, simple, predictable pages.

Rule 1: use the right HTML element

This is the most powerful rule. Browsers and screen readers already understand real HTML elements. A real button can be reached with Tab, pressed with Enter and Space, and is announced as a button, all for free. A div with an onClick has none of that.

// Bad: looks like a button, but a keyboard and a screen reader cannot use it
<div className="btn" onClick={save}>Save</div>

// Good
<button type="button" onClick={save}>Save</button>
  • Use button for actions and a (or Link) for navigation.
  • Use headings h1 to h6 in order, with one h1 per page. They are the outline of your page.
  • Use nav, main, header, footer, section and article for the page layout.
  • Use ul and li for lists, and table with th for real tables of data.

These elements are called semantic HTML. Using them gives you most of accessibility with no extra code.

Rule 2: label everything

  • Every form field needs a visible label, connected with htmlFor and id. A placeholder is not a label.
  • Every meaningful image needs alt text that says what it means. A decorative image gets an empty alt="" so screen readers skip it.
  • Buttons that show only an icon need a name: <button aria-label="Close">X</button>.
  • Links should make sense alone. “Read our pricing guide” is better than “click here”.
<label htmlFor="email">Email address</label>
<input id="email" type="email" autoComplete="email" />

<img src={product.photo} alt={'Red running shoe, side view'} />
<img src="/divider.png" alt="" />

In React, you can make unique ids with the useId hook, which is safe to use when a component appears many times on one page:

import { useId } from 'react';

function TextField({ label }) {
  const id = useId();
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </div>
  );
}

Rule 3: the keyboard must work

Put your mouse away and try your app with only the keyboard. Can you reach everything with Tab? Can you see where you are? Can you press every button with Enter or Space? Can you leave a modal with Escape?

  • Keep the focus outline visible. Never write outline: none without giving a clear replacement, such as a coloured ring with :focus-visible.
  • The Tab order should follow the visual order. Avoid positive tabIndex values. Use tabIndex={0} only for rare custom controls, and tabIndex={-1} to allow focusing an element from code.
  • Add a skip link at the top of the page that jumps to the main content, so keyboard users do not Tab through the whole menu on every page.
  • Never trap focus, except inside an open modal, where it must stay until the modal closes.
<a href="#main" className="skip-link">Skip to main content</a>
...
<main id="main" tabIndex={-1}>...</main>

Rule 4: do not rely on colour alone

  • Text needs enough contrast with its background. The common guide is at least 4.5 to 1 for normal text, and 3 to 1 for large text. Browser tools can check this for you.
  • Do not show an error only with red. Add an icon or text such as “Error: ...”.
  • Links inside text should be underlined or otherwise clearly different, not only a different colour.
  • Respect reduced motion: some people feel sick from animation. Use @media (prefers-reduced-motion: reduce) to turn it off.

ARIA: use it when HTML is not enough

ARIA attributes add extra information for assistive technology. The first rule of ARIA is: do not use ARIA if a normal HTML element can do the job. Wrong ARIA is worse than none. When you really need it, these are the most common attributes:

  • aria-label: gives a name to an element that has no visible text.
  • aria-labelledby: names an element using another element's text.
  • aria-describedby: connects help or error text to a field.
  • aria-expanded and aria-controls: tell if a menu or panel is open, and what it controls.
  • aria-live="polite" or role="status": announces changes, such as “Item added to cart”.
  • role="alert": announces an important message at once, such as an error.
  • aria-hidden="true": hides decoration from screen readers.
function Accordion({ title, children }) {
  const [open, setOpen] = useState(false);
  const panelId = useId();
  return (
    <div>
      <button aria-expanded={open} aria-controls={panelId} onClick={() => setOpen(!open)}>
        {title}
      </button>
      <div id={panelId} hidden={!open}>{children}</div>
    </div>
  );
}

Announce changes

When something changes on the page without a reload (a message, a count, a loaded list), a screen reader user may not notice. A live region announces it:

<p role="status" aria-live="polite">{cartCount} items in your cart</p>

{error && <p role="alert">{error}</p>}

Keep announcements short, and do not announce every tiny change.

Single page apps: route changes

In a normal website, loading a new page tells the screen reader that something changed. In a React app with a router, the page changes silently. Help the user by moving focus and updating the title:

import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';

function RouteAnnouncer() {
  const { pathname } = useLocation();
  const headingRef = useRef(null);

  useEffect(() => {
    document.title = 'Page: ' + pathname + ' | My App';
    document.querySelector('h1')?.focus();
  }, [pathname]);

  return null;
}
// give each page's <h1 tabIndex={-1}> so it can receive focus

Also give every page a unique, helpful document.title.

Test accessibility

  • Use only the keyboard for a few minutes. It reveals most problems.
  • ESLint plugin `jsx-a11y` warns about problems as you type (it is included in many React starters).
  • Lighthouse (in Chrome) and the axe DevTools extension scan a page for common problems.
  • Turn on a screen reader: VoiceOver on Mac and iPhone, TalkBack on Android, NVDA (free) on Windows. Even a short try is eye-opening.
  • React Testing Library queries like getByRole and getByLabelText fail when your markup is not accessible.

A quick checklist

  • Real button, a, nav, main, and heading elements in the right order.
  • Every field has a label, every image has suitable alt.
  • Everything works with the keyboard and the focus is always visible.
  • Text contrast is good, and colour is never the only signal.
  • Errors and changes are announced, and route changes are handled.
  • Animation can be reduced, and text can be zoomed to 200 percent without breaking.

Common mistakes

  • Using div for buttons and links.
  • Removing the focus outline.
  • Using placeholder text as the only label.
  • Adding ARIA that does not match what the element really does.
  • Skipping heading levels or using headings only for size.
  • Forgetting that route changes are silent.

Your task

  1. Unplug your mouse and use your app with only the keyboard. Write down every problem.
  2. Fix one: replace a div button with a real button, and add a visible focus style.
  3. Add a skip link and make sure every form field has a label.
  4. Run Lighthouse and fix the first three accessibility issues it reports.

How to check it worked

  • You can reach and use every control with only the keyboard.
  • A screen reader reads each field with its label.
  • Lighthouse shows a high accessibility score and no contrast errors.

Summary

  • Use semantic HTML first: real buttons, links, headings and landmarks.
  • Label fields, add alt text, keep focus visible, and make everything work with a keyboard.
  • Do not rely on colour alone, and keep good contrast.
  • Use ARIA only when needed, announce changes, handle route changes, and test with a keyboard, tools and a screen reader.

Frequently asked questions

What is accessibility in web development?

Building websites and apps that people with disabilities, and everyone else, can use, including with keyboards, screen readers and zoom.

Do I need ARIA for everything?

No. Use semantic HTML first. Add ARIA only when HTML cannot do the job. Wrong ARIA can make things worse.

How do I test accessibility in React?

Use the keyboard only, run Lighthouse or axe, use the jsx-a11y ESLint plugin, and try a screen reader. Automated tools find only part of the problems.

What is the minimum colour contrast?

A common guide is 4.5 to 1 for normal text and 3 to 1 for large text. Browser developer tools can show the ratio.