Accessibility in React
Semantic HTML, labels, keyboard support, focus management, ARIA and testing with assistive tech.
Accessibility (often shortened to a11y) means making your app usable by everyone — including people who use screen readers, navigate with only a keyboard, have low vision or colour blindness, or have motor or cognitive disabilities. Around one in six people worldwide lives with a disability, and many more face temporary or situational limits (a broken arm, bright sunlight, a slow connection). Accessible apps are also better for everyone, rank better in search and are a legal requirement in many places.
React renders normal HTML, so web accessibility rules apply directly. This lesson covers the React-specific details and the habits that matter most.
Use semantic HTML#
The single most effective thing you can do is use the right element:
Quick guide:
Screen-reader users often navigate by headings and landmarks, so good structure is like a table of contents for them.
Tip: fragments (
<>…</>) let you return several elements without wrapper<div>s that break HTML structure, such as rows inside a<table>or items inside a<ul>.
Accessible forms#
Every input needs a label. In JSX it's htmlFor, not for. For reusable components, generate ids with useId so two instances never clash:
aria-describedbymakes screen readers read the hint and error after the label.aria-invalidannounces that the field has an error.autoCompletehelps everyone, especially people with motor or memory difficulties.- Placeholders aren't labels — they disappear when typing and often have poor contrast.
- Group related radio buttons and checkboxes with
<fieldset>and<legend>.
Images and icons#
Every <img> needs an alt attribute — descriptive for meaningful images, empty (alt="") for decorative ones.
Keyboard support and focus#
Everything you can do with a mouse should work with a keyboard: Tab moves forwards, Shift+Tab backwards, Enter/Space activate, and Escape closes things. Test your app by putting the mouse away.
Never remove focus outlines without a replacement. Use :focus-visible so keyboard users get a clear ring:
Managing focus with refs
When content changes, move focus to where the user needs to be. For example, after a client-side route change, focus the new page's heading so screen-reader users hear where they are:
tabIndex={-1} lets an element receive focus from code without adding it to the Tab order. Avoid positive tabIndex values — they scramble the natural order.
Accessible dialogs
The native <dialog> element with showModal() gives you focus trapping, Escape-to-close and an inert background for free:
The browser returns focus to the element that opened the dialog when it closes.
Announcing dynamic changes#
Screen readers don't notice text that appears on its own. Use a live region for status messages:
role="status"/aria-live="polite"waits for the user to pause.role="alert"interrupts immediately — save it for errors.- The live region element should already be on the page before its text changes.
ARIA: use it wisely#
ARIA attributes (role, aria-*) change what assistive technology announces. They add no behaviour — role="button" on a div doesn't make it focusable or clickable by keyboard. The first rule of ARIA: if a native element does the job, use it.
ARIA is genuinely useful for states native HTML can't express:
Other common ones: aria-current="page" for the active nav link (React Router's NavLink adds it for you), aria-pressed for toggle buttons, and aria-busy while content loads. For complex widgets (comboboxes, menus, tabs, date pickers) use a well-tested library such as React Aria, Radix UI or Headless UI rather than building from scratch.
Colour, motion and text#
- Contrast: body text needs a contrast ratio of at least 4.5:1 (3:1 for large text) under WCAG AA. DevTools shows the ratio in the colour picker.
- Don't rely on colour alone: pair red/green with icons or text ("✔ Paid", "✖ Failed").
- Respect reduced motion:
- Use relative units (
rem) so text scales with browser settings, and make sure layouts work at 200% zoom.
Testing accessibility#
- Keyboard: unplug the mouse and complete the main tasks.
- Screen reader: try VoiceOver (macOS/iOS, Cmd+F5), NVDA (Windows, free) or TalkBack (Android).
- Automated checks: Lighthouse and the axe DevTools extension find many issues (missing labels, low contrast, missing alt text).
- Lint:
eslint-plugin-jsx-a11yflags problems as you type; oxlint includes many of the same rules. - Tests: React Testing Library's
getByRolequeries fail when elements lack accessible roles or names — a free a11y check in every test.
Automated tools catch roughly a third to a half of issues; manual testing finds the rest.
Common mistakes#
- Clickable
<div>s and<span>s instead of buttons and links. - Inputs without labels, or using placeholder text as the label.
- Removing focus outlines with
outline: none. - Missing or useless alt text (
alt="image"). - Adding ARIA roles that contradict the element (
<button role="link">). - Modals that don't trap focus or can't be closed with Escape.
- Skipping heading levels for visual size — style with CSS instead.
What's next#
Your app is fast, tested and accessible. Time to share it with the world: deploying React apps.
Check your understanding
Quick quiz
1.Why is
<div onClick={save}>Save</div>worse than<button onClick={save}>Save</button>?2.How do you connect a
<label>to an<input>in JSX?3.What is the first rule of ARIA?
Finished reading?
Mark this lesson complete to track your progress.