React · Auth & Quality

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.

Advanced 6 min Lesson 36 of 42Examples target React 19 with Vite.
Before you start:Forms
0/42 in React
On this page · 6 min · Advanced

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#

JSX Illustrative
<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#

JSXAddToCart.jsx Runnable
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>
  );
}
Expected output
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#

  1. Real <button> elements give keyboard and screen-reader support for free.
  2. The image has descriptive alt text and fixed dimensions.
  3. The native <dialog> with showModal() traps focus and supports Escape.
  4. aria-labelledby names the dialog after its heading.
  5. 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#

Clickable divs

<div onClick> cannot be focused or activated with the keyboard. Use <button>.

Placeholder as label

Placeholders disappear when typing and are often not announced. Use a real label.

Lost focus after navigation or modals

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?
<button aria-label="Menu" aria-expanded={open}> and <label htmlFor="search">.
Show solution
JSX Runnable
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#

Accessibility quizMultiple choice Question 1 of 3

Which element should perform an in-page action like “Add to cart”?

Score so far: 0/0

Want more? Practise all React MCQs · React interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.