Accessibility in React
By the end of this lesson you'll know how to build React UIs everyone can use: semantic HTML, the htmlFor / id label pattern, ARIA attributes and roles, keyboard navigation and focus management, focus trapping in modals, announcing route changes with live regions, alt text, color contrast, and eslint-plugin-jsx-a11y .
Learn Accessibility in React in our free React course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free React course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
1. Semantic HTML first
The single biggest accessibility win is using the right element for the job . A , , , or comes with focus, keyboard support, and the correct screen-reader role for free. A gives you none of that — you'd have to rebuild it all by hand, and you'll get it subtly wrong.
2. Labels, aria-* and roles
Every form control needs an accessible name . The cleanest way is a real tied to the input with htmlFor (JSX's name for the HTML for attribute) matching the input's id . When there's no visible text — an icon-only button — give it a name with aria-label , and hide decorative icons with aria-hidden="true" . Reach for a role only when no native element conveys the meaning.
Run this to see how a browser computes an element's accessible name, and why an icon-only button with no label is announced as just "button":
Your turn. An icon button needs an aria-label and its decorative icon should be hidden from screen readers. Fill in the two blanks, then run it and check the output.
3. Keyboard navigation & focus management
Many users navigate with the keyboard alone — Tab to move, Enter/Space to activate, Escape to close. Native elements are reachable by default; custom widgets need care. When the UI changes (a form opens, an error appears), you often need to move focus programmatically: grab the node with a useRef and call ref.current.focus() , usually in a useEffect .
Trap focus: while a modal is open, Tab and Shift+Tab should cycle only through the dialog's focusable elements, never the page behind it.
Move focus in: when it opens, focus the first control (or the dialog itself). When it closes, return focus to the element that opened it.
Escape closes it , and the backdrop should be inert. Use role="dialog" with aria-modal="true" and an aria-labelledby pointing at the title.
Tip: libraries like Radix UI / Headless UI implement all of this correctly so you don't have to.
4. Announcing route changes (why SPAs need extra care)
In a traditional site, clicking a link loads a new page and the screen reader announces it. In a single-page app , the router swaps content with JavaScript — no page load happens — so assistive tech is never told anything changed. You must announce it yourself with an aria-live region, and move focus to the new page's heading.
5. Alt text, color contrast & linting
Alt text: every needs an alt attribute. Describe the content or purpose for meaningful images; use an empty alt="" for purely decorative ones so they're skipped. Color contrast: text must stand out from its background — WCAG recommends a contrast ratio of at least 4.5:1 for normal text — or low-vision users can't read it. Linting: add eslint-plugin-jsx-a11y to catch missing alt text, unlabeled inputs, and bad ARIA as you type. It won't catch everything, but it catches the easy mistakes automatically.
for is a reserved word in JavaScript, so JSX uses htmlFor . Its value must match the input's id to associate the label.
No — the opposite. Prefer native semantic elements, which carry the right roles automatically. Add ARIA only when no native element fits.
Q: Why do SPAs need special accessibility handling?
Client-side routing changes content without a full page load, so screen readers aren't notified. You announce the change with a live region and move focus to the new heading.
Q: Will eslint-plugin-jsx-a11y make my app accessible?
It catches many common static mistakes, but not everything. Combine it with keyboard testing and a real screen reader for full coverage.
No blanks this time — just a brief and a starter array. Write auditField(field) that flags fields missing an accessible name. Run it and check your output against the expected lines in the comments.
Practice quiz
Why should you prefer a button element over a div with an onClick?
- A real button is focusable, keyboard-operable, and announced as a button by screen readers for free
- Divs are slower to render
- Divs cannot have click handlers
- There is no real difference
Answer: A real button is focusable, keyboard-operable, and announced as a button by screen readers for free. Semantic elements like button come with focus, Enter/Space activation, and the correct role built in; a div needs all of that added manually.
How do you correctly associate a label with an input in JSX?
- Add a title attribute to the input
- Use the for attribute on the label
- Use htmlFor on the label matching the input's id
- Wrap the input in a span
Answer: Use htmlFor on the label matching the input's id. In JSX you write htmlFor (not for) on the label, and its value must match the input's id.
How do you move keyboard focus to an element in React?
- Call element.click()
- Get the node via a ref and call ref.current.focus()
- Add tabindex=0 only
- Set autofocus in CSS
Answer: Get the node via a ref and call ref.current.focus(). You attach a ref to the element and call ref.current.focus() (often in an effect) to move keyboard focus to it.
What does an aria-live region do?
- It traps focus inside a modal
- It disables an element
- It sets the page language
- It announces dynamic content changes to screen readers without moving focus
Answer: It announces dynamic content changes to screen readers without moving focus. An aria-live region (e.g. aria-live=polite) tells assistive tech to announce updates to that region as they happen.
Why do single-page apps (SPAs) need extra accessibility care for navigation?
- A client-side route change does not reload the page, so screen readers are not told the page changed
- They load too fast
- They use too much JavaScript
- They cannot use semantic HTML
Answer: A client-side route change does not reload the page, so screen readers are not told the page changed. Because SPAs swap content without a full page load, you must manually announce route changes and manage focus.
What is focus trapping in a modal dialog?
- Hiding the modal from keyboard users
- Keeping Tab focus cycling within the modal until it is closed
- Disabling the keyboard
- Auto-closing the modal after 5 seconds
Answer: Keeping Tab focus cycling within the modal until it is closed. A modal should trap focus so Tab and Shift+Tab cycle only through the dialog's controls, not the page behind it.
What should the alt attribute contain for a meaningful image?
- The image file name
- The word 'image'
- Always an empty string
- A concise description of the image's content or purpose
Answer: A concise description of the image's content or purpose. alt should describe the image's content or function; purely decorative images use an empty alt (alt='') to be skipped.
Why does color contrast matter for accessibility?
- It changes the tab order
- It is required by JSX
- Low contrast text is hard to read for users with low vision or in bright light
- It makes the site load faster
Answer: Low contrast text is hard to read for users with low vision or in bright light. Sufficient contrast between text and background (WCAG suggests at least 4.5:1 for normal text) keeps content readable.
What does eslint-plugin-jsx-a11y do?
- It fixes all accessibility issues automatically at runtime
- It lints JSX for common accessibility problems during development
- It replaces screen readers
- It adds ARIA roles to every element
Answer: It lints JSX for common accessibility problems during development. eslint-plugin-jsx-a11y statically warns about accessibility mistakes (missing alt, bad ARIA, unlabeled inputs) as you code.
When should you add an ARIA role to an element?
- Only when a native semantic element with that meaning is not available
- Only on div elements
- On every element for safety
- Never, ARIA is deprecated
Answer: Only when a native semantic element with that meaning is not available. The first rule of ARIA: prefer a native element. Add a role only when no semantic HTML element conveys the meaning.