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
buttonfor actions anda(orLink) for navigation. - Use headings
h1toh6in order, with oneh1per page. They are the outline of your page. - Use
nav,main,header,footer,sectionandarticlefor the page layout. - Use
ulandlifor lists, andtablewiththfor 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 withhtmlForandid. A placeholder is not a label. - Every meaningful image needs
alttext that says what it means. A decorative image gets an emptyalt=""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: nonewithout giving a clear replacement, such as a coloured ring with:focus-visible. - The Tab order should follow the visual order. Avoid positive
tabIndexvalues. UsetabIndex={0}only for rare custom controls, andtabIndex={-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-expandedandaria-controls: tell if a menu or panel is open, and what it controls.aria-live="polite"orrole="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
getByRoleandgetByLabelTextfail 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
divfor 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
- Unplug your mouse and use your app with only the keyboard. Write down every problem.
- Fix one: replace a
divbutton with a real button, and add a visible focus style. - Add a skip link and make sure every form field has a label.
- 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
alttext, 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.