Accessibility
Accessibility means everyone can use your app — with a keyboard, a screen reader, zoom or high contrast. In React, most of it comes from using the right HTML and managing focus.
On this page · 6 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Use semantic elements and labels in JSX
- Make everything keyboard-operable
- Manage focus for dialogs and dynamic content
- Announce updates with aria-live and test with tools
Explanation#
Semantic HTML first: <button> for actions, <a href>/<Link> for navigation, <nav>, <main>, headings in order, <ul> for lists. A div with onClick is invisible to keyboards and screen readers.
Every input needs a label (<label htmlFor> — remember htmlFor in JSX). Images need alt text (alt="" for decorative ones).
Keyboard: everything clickable must be reachable with Tab and work with Enter/Space; never remove focus outlines without a visible replacement.
Focus management: when a dialog opens, move focus into it and return focus to the trigger when it closes. The native <dialog> element with showModal() traps focus and handles Escape for you.
Announce dynamic changes (“Added to cart”, form errors) with aria-live="polite" regions or role="alert".
Test with keyboard-only use, a screen reader (NVDA, VoiceOver), Lighthouse and axe; eslint-plugin-jsx-a11y catches many issues while coding.
Syntax#
<label htmlFor="email">Email</label>
<input id="email" type="email" aria-describedby="email-help" />
<p id="email-help">We never share it.</p>
<div aria-live="polite">{status}</div>
Code example: An accessible dialog and live announcements#
import { useRef, useState } from 'react';
export default function AddToCart() {
const dialogRef = useRef(null);
const [qty, setQty] = useState(1);
const [message, setMessage] = useState('');
function openDialog() {
dialogRef.current.showModal(); // native modal: focus moves inside, Escape closes
}
function confirm(e) {
e.preventDefault();
dialogRef.current.close(); // focus returns to the button that opened it
setMessage(`Added ${qty} notebook${qty > 1 ? 's' : ''} to your cart.`);
}
return (
<main>
<h1>Spiral Notebook</h1>
<img src="https://picsum.photos/200/120" alt="Blue spiral notebook, A5 size" width={200} height={120} />
<button onClick={openDialog}>Add to cart</button>
<dialog ref={dialogRef} aria-labelledby="dlg-title">
<form onSubmit={confirm}>
<h2 id="dlg-title">Choose quantity</h2>
<label htmlFor="qty">Quantity</label>
<input id="qty" type="number" min={1} max={10} value={qty} onChange={e => setQty(Number(e.target.value))} />
<button type="submit">Confirm</button>
<button type="button" onClick={() => dialogRef.current.close()}>Cancel</button>
</form>
</dialog>
<p aria-live="polite">{message}</p>
</main>
);
}
Keyboard users can Tab to “Add to cart” and press Enter. The dialog opens with focus inside it; Tab stays within the dialog, Escape or Cancel closes it, and focus returns to the button. After confirming, screen readers announce “Added 2 notebooks to your cart.” from the live region without moving focus.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- Real <button> elements give keyboard and screen-reader support for free.
- The image has descriptive alt text and fixed dimensions.
- The native <dialog> with showModal() traps focus and supports Escape.
- aria-labelledby names the dialog after its heading.
- The aria-live paragraph announces the result politely.
Real-world use case#
A government services portal must meet WCAG 2.2 AA. Using semantic elements, labelled forms, managed focus and live regions lets screen-reader users complete applications without help.
Common mistakes#
<div onClick> cannot be focused or activated with the keyboard. Use <button>.
Placeholders disappear when typing and are often not announced. Use a real label.
If focus stays on a removed element, keyboard users are lost. Move focus to the new content’s heading or back to the trigger.
Best practices#
- Semantic HTML before ARIA
- Label every input and name every button
- Manage focus for dialogs and route changes
- Test with keyboard, screen reader and axe
Remember this#
Button for actions, link for navigation.
Every input has a label; every image has alt.
Dialogs: focus in, focus back. Updates: aria-live.
Practice exercise#
Your task: Fix this component: <div onClick={toggle}>☰</div><input placeholder="Search">. Make the menu toggle a real button with an accessible name and state, and give the input a label.
Need a hint?
Show solution
import { useState } from 'react';
export default function Header() {
const [open, setOpen] = useState(false);
return (
<header>
<button aria-label="Menu" aria-expanded={open} onClick={() => setOpen(o => !o)}>☰</button>
{open && <nav aria-label="Main">Home · Shop</nav>}
<label htmlFor="search">Search</label>
<input id="search" type="search" />
</header>
);
}
Mini quiz#
Which element should perform an in-page action like “Add to cart”?
Want more? Practise all React MCQs · React interview questions
My notes#
Saved in this browser.