Web Accessibility in React: A Practical Guide for 2026

By Eoin Villarin, 2025-11-15, updated 2026-10-28

Almost every top website still fails basic accessibility checks. This 2026 guide covers semantic HTML, focus management, ARIA that helps, WCAG 2.2 and a React testing workflow that sticks.

How do I improve React accessibility and meet WCAG 2.2 in my app?

Improving React accessibility starts with semantic HTML: real buttons and links instead of clickable divs, labels on every input and a logical heading structure. Then manage focus on route changes and modals, add ARIA only where no native element exists, target WCAG 2.2 Level AA, and combine automated axe checks with keyboard and screen reader testing.

Key takeaways

  • The 2026 WebAIM Million found detectable WCAG failures on 95.9% of the top one million home pages.
  • WebAIM found pages with ARIA averaged 59.1 detected errors, compared with 42 on pages without ARIA.
  • Missing form labels appeared on 51% of top home pages in 2026, according to WebAIM.
  • The @axe-core/react package does not support React 18 and above, so use the Playwright axe integration instead.
  • WCAG 2.2 Target Size Minimum (2.5.8) says interactive targets should be at least 24 by 24 CSS pixels or well spaced.

Good React accessibility is not a nice extra you bolt on before launch. It decides whether people using screen readers, keyboards, voice control or magnification can actually buy from you, book with you or use your product. And the web is not getting better on its own.

The 2026 WebAIM Million report found detectable WCAG failures on 95.9% of the top one million home pages, up from 94.8% a year earlier, with an average of 56.1 errors per page. Pages built with React averaged 43.5 errors, which is better than average but still far from usable for many people.

In this guide you will learn the patterns that fix most real-world problems in React apps: semantic HTML, focus management, the small set of ARIA attributes that matter, accessible forms, the WCAG 2.2 criteria teams usually miss, and a testing workflow that catches regressions before your users do.

Why React accessibility matters more in 2026

Three things changed the stakes. First, regulation. The European Accessibility Act has applied since 28 June 2025 to many online stores and digital services sold into the EU, and I cover what it requires in my European Accessibility Act guide. Second, WCAG 2.2 is now the reference standard most audits use.

Third, the code itself is getting heavier. WebAIM found that ARIA usage grew 27% in one year, and pages with ARIA averaged 59.1 detected errors compared with 42 on pages without it. More attributes do not mean more accessibility. Usually they mean someone patched a div instead of using the right element.

The good news is that the most common failures are predictable. Low contrast text, missing alt text, missing form labels, empty links and empty buttons account for the vast majority of detected errors, and every one of them is fixable at the component level.

Start with semantic HTML, not ARIA

The highest-leverage fix in most React codebases is replacing clickable div and span elements with real HTML. A native button is focusable, responds to Enter and Space, and announces itself as a button. A div with an onClick does none of that.

Printed physical map of the world, like landmarks that help screen reader users navigate
Landmarks and headings give screen reader users a map. Photo: rawpixel, CC0
// Before: invisible to keyboards and screen readers
<div className="btn" onClick={save}>Save</div>

// After: focusable, keyboard operable and announced correctly
<button type="button" className="btn" onClick={save}>
  Save changes
</button>

Screen reader users navigate by landmarks and headings, so give every page a clear structure:

  • One main element per page, plus header, nav and footer where they apply.
  • A logical heading order with one h1 and no skipped levels. Style headings with CSS, not by picking a smaller tag.
  • Links for navigation, buttons for actions. If it changes the URL, it is a link. If it does something on the page, it is a button.
  • Lists for lists, so assistive technology can announce "list, 6 items" before reading them.
  • A lang attribute on the html element, which 13.5% of top home pages still miss.

How to manage focus in a single-page React app

When a traditional page loads, the browser resets focus to the top of the document and screen readers announce the new title. Client-side routing skips both. Focus stays on the link the user just activated, which may no longer exist on screen.

Next.js includes a built-in route announcer that reads the new page title after client-side navigations, so give every page a unique title. In React Router and custom setups, move focus to the main heading yourself:

import { useEffect, useRef, type ReactNode } from 'react'
import { useLocation } from 'react-router'

export function PageHeading({ children }: { children: ReactNode }) {
  const ref = useRef<HTMLHeadingElement>(null)
  const isFirstRender = useRef(true)
  const { pathname } = useLocation()

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false
      return
    }
    ref.current?.focus()
  }, [pathname])

  return <h1 ref={ref} tabIndex={-1}>{children}</h1>
}

The tabIndex={-1} makes the heading focusable by script without adding it to the tab order, and skipping the first render avoids stealing focus on initial page load.

Use the native dialog element for modals

Custom modals are where focus bugs pile up. The native dialog element opened with showModal() makes the rest of the page inert, closes on Escape and renders a styleable backdrop, as the MDN dialog reference explains.

import { useEffect, useId, useRef, type ReactNode } from 'react'

type Props = { open: boolean; onClose: () => void; title: string; children: ReactNode }

export function Modal({ open, onClose, title, children }: Props) {
  const ref = useRef<HTMLDialogElement>(null)
  const titleId = useId()

  useEffect(() => {
    const dialog = ref.current
    if (!dialog) return
    if (open && !dialog.open) dialog.showModal()
    if (!open && dialog.open) dialog.close()
  }, [open])

  return (
    <dialog ref={ref} onClose={onClose} aria-labelledby={titleId}>
      <h2 id={titleId}>{title}</h2>
      {children}
      <button type="button" onClick={onClose}>Close</button>
    </dialog>
  )
}

After closing, make sure focus lands back on the button that opened the dialog, and test that in every browser you support.

Add a working skip link

Only 17.1% of top home pages have a skip link, and WebAIM found one in ten of those were broken. Put a visible-on-focus link to #main as the first focusable element, and make sure the target actually exists.

Which ARIA patterns actually matter?

The first rule of ARIA is to use a native element when one exists. When it does not, these are the attributes I use most in React components:

PatternAttributeUse it for
Icon-only buttonsaria-labelClose, search, menu and social icons
Disclosure and menusaria-expanded, aria-controlsAccordions, dropdowns, mobile navigation
Dynamic updatesaria-live="polite"Search result counts, cart totals, saved states
Urgent messagesrole="alert"Submission errors and session timeouts
Field hints and errorsaria-describedby, aria-invalidForm validation messages
Current locationaria-current="page"Active navigation links and breadcrumbs

Live regions must exist in the DOM before their content changes, so render the container on mount and update its text later. Avoid aria-hidden="true" on anything that contains focusable elements, because keyboard users can still tab into content that screen readers cannot see.

Every ARIA attribute is a promise to assistive technology. If you add role="menu" but do not implement arrow key navigation, you have made the component harder to use, not easier.

WebAIM found that 22% of ARIA menus introduced barriers for exactly this reason. For complex widgets like comboboxes, tabs and tree views, follow the ARIA Authoring Practices Guide or use a tested headless library such as Radix UI or React Aria.

How to build accessible forms in React

Missing form labels appeared on 51% of top home pages in 2026. Every input needs a programmatic label, and errors need to be connected to the field they describe. React's useId hook generates stable IDs that work with server rendering:

Close-up of light grey keyboard keys, since every form should work by keyboard alone
Every form should work with a keyboard alone. Photo: Topher, CC0
import { useId } from 'react'

export function EmailField({ error }: { error?: string }) {
  const id = useId()
  const errorId = `${id}-error`

  return (
    <div>
      <label htmlFor={id}>Email address</label>
      <input
        id={id}
        name="email"
        type="email"
        autoComplete="email"
        aria-invalid={error ? true : undefined}
        aria-describedby={error ? errorId : undefined}
      />
      {error && <p id={errorId}>{error}</p>}
    </div>
  )
}

On submit, move focus to the first invalid field or to an error summary at the top of the form. Never rely on colour alone to show an error, and keep placeholder text as a hint rather than a replacement for the label.

Which WCAG 2.2 criteria do React teams miss?

WCAG 2.2 added criteria that catch modern UI patterns. These are the ones React teams most often overlook:

  • Focus Not Obscured (2.4.11): sticky headers, cookie banners and chat widgets must not completely cover the focused element.
  • Target Size Minimum (2.5.8): interactive targets should be at least 24 by 24 CSS pixels, or have enough spacing around them.
  • Dragging Movements (2.5.7): drag-and-drop lists, sliders and kanban boards need a single-pointer alternative such as buttons.
  • Accessible Authentication (3.3.8): do not block paste in password fields or require puzzles to log in. Support password managers and passkeys.
  • Redundant Entry (3.3.7): do not make users retype information they already gave you in the same process, such as a shipping address.

Also check that visible focus styles survive your CSS reset. A global outline: none is still one of the most common keyboard failures in React apps.

How to test React accessibility: automated and manual

Automated tools are fast and catch regressions, but WebAIM is clear that not all conformance failures can be detected automatically. You need both layers.

Black headphones with a red cord, ready for manual screen reader testing
Automated checks plus manual screen reader testing. Photo: rawpixel, CC0

Automated checks in your pipeline

  • Linting: eslint-plugin-jsx-a11y flags missing alt text, invalid ARIA and click handlers on non-interactive elements as you type.
  • Component tests: query with Testing Library's getByRole and accessible names. If a test cannot find the button by role and name, neither can a screen reader.
  • Storybook: the accessibility addon runs axe against every story. My guide to building a design system with React and Storybook shows how to wire it into component reviews.
  • End-to-end: run axe against real pages in Playwright, following the Playwright accessibility testing docs.
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

test('checkout has no detectable WCAG A/AA violations', async ({ page }) => {
  await page.goto('/checkout')
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
    .analyze()
  expect(results.violations).toEqual([])
})

One update for older tutorials: the @axe-core/react package does not support React 18 and above, so use the Playwright integration or browser extensions instead.

A manual testing routine for every release

  1. Unplug the mouse and complete the main user journey with Tab, Shift+Tab, Enter, Space, Escape and arrow keys.
  2. Run the same journey with a screen reader: VoiceOver on macOS and iOS, NVDA on Windows, TalkBack on Android.
  3. Zoom the browser to 200% and 400% and confirm nothing overlaps or disappears.
  4. Turn on reduced motion and check that animations respect prefers-reduced-motion.

Accessible markup also helps search engines understand your pages. My Next.js SEO guide covers how headings, metadata and structure overlap with SEO.

Frequently asked questions

Is React accessible by default?

No. React renders whatever HTML you write, so accessibility depends on the elements and attributes your components output. React does help with tools like useId for linking labels and errors, and frameworks such as Next.js add a route announcer. But clickable divs, missing labels and broken focus handling are just as possible in React as anywhere else.

Which WCAG level should a React app meet?

Aim for WCAG 2.2 Level AA. It is the level referenced by most accessibility laws and procurement policies, including the standards used for the European Accessibility Act. Level AAA is worth pursuing for specific criteria where it is practical, but it is rarely required for an entire site or application.

Can automated tools make my React app accessible?

They help, but they cannot finish the job. Tools like axe and WAVE reliably catch issues such as missing labels, contrast failures and invalid ARIA, yet they cannot judge whether alt text is meaningful, focus order makes sense or a custom widget is usable with a screen reader. Combine automated checks with regular keyboard and screen reader testing.

Should I use a component library for accessibility?

For complex widgets, yes. Headless libraries like Radix UI and React Aria implement keyboard interaction, focus management and ARIA patterns that take weeks to build and test yourself. You still need to provide good labels, contrast and content, and you should test the final result, because a library cannot fix how you compose it.

Next steps: make accessibility part of your workflow

Start with the biggest React accessibility wins: real buttons and links, labels on every input, visible focus styles, sensible contrast and focus handling on route changes and modals. Then add linting and Playwright checks so new code cannot quietly undo your work, and schedule manual screen reader passes before each major release.

If you want an accessibility review of your React app, Shopify store or WordPress site, contact me with your project details. You can also see the kind of sites I build in my client case studies.

Related reading