Understanding Accessibility in React
Accessibility means designing interfaces that people with different abilities can understand and operate. In React applications, this includes semantic markup, keyboard navigation, meaningful form labels, visible focus indicators, appropriate color contrast, and predictable behavior when components update dynamically.
React renders standard HTML elements, so many web accessibility practices apply directly. However, conditional rendering, client-side navigation, custom widgets, and dynamic forms introduce additional responsibilities for focus management and communicating changes to assistive technologies.
Start with native HTML
Use semantic HTML elements before adding ARIA attributes. A native button already supports keyboard activation, focus, and button semantics. A clickable div does not provide these behaviors automatically.
Why Accessibility Matters
An accessible interface supports people who use screen readers, keyboard navigation, magnification, voice input, or other assistive technologies. It also helps users working on mobile devices, navigating unfamiliar interfaces, or dealing with temporary limitations.
- Keyboard users need to access interactive controls without a mouse.
- Screen reader users need meaningful names, roles, states, and relationships.
- People with low vision need readable text, sufficient contrast, and layouts that support zooming.
- Users with cognitive disabilities benefit from predictable navigation and clear instructions.
- People sensitive to motion may need animations to be reduced or removed.
Use Semantic HTML Elements
Semantic HTML communicates the purpose of content and controls to browsers and assistive technologies. Use headings for document structure, links for navigation, buttons for actions, and form elements for data entry.
function AccountActions() {
return (
<main>
<h1>Account Settings</h1>
<section aria-labelledby="profile-heading">
<h2 id="profile-heading">Profile</h2>
<p>Manage your personal information.</p>
<a href="/profile">Edit profile</a>
<button type="button">Sign out</button>
</section>
</main>
);
}
The anchor navigates to a page, while the button performs an action. Using the correct element gives users familiar interaction behavior without requiring additional JavaScript to recreate native functionality.
Avoid using generic elements such as div and span as interactive controls when a native button, link, or form element already meets the requirement.
Create a Meaningful Heading Structure
Headings help users understand page organization and move directly to relevant sections. Choose heading levels according to the document hierarchy, rather than selecting them only for their visual appearance.
function DocumentationPage() {
return (
<main>
<h1>React Accessibility</h1>
<section>
<h2>Keyboard Navigation</h2>
<p>Learn how users navigate interactive controls.</p>
<h3>Focus Indicators</h3>
<p>Make the current keyboard target visible.</p>
</section>
</main>
);
}
Use landmarks such as main, nav, and header when they accurately describe the page structure. Avoid adding unnecessary landmarks or choosing heading levels that do not reflect the content relationships.
Accessible Buttons and Links
Interactive controls need accessible names that communicate their purpose. Prefer descriptive button text and meaningful link text instead of vague labels such as "Click here" when more useful wording is available.
function DocumentActions() {
return (
<div>
<button type="button" onClick={saveDocument}>
Save document
</button>
<a href="/documentation/react-hooks">
Read the React Hooks guide
</a>
</div>
);
}
Native buttons support keyboard activation, and native anchors support browser navigation features. Icon-only controls need an accessible name, supplied through visible text or an appropriate attribute such as aria-label.
Do not add redundant ARIA roles to native elements when their existing semantics already communicate the correct purpose. For example, a button does not need an additional role="button".
Support Keyboard Navigation
Keyboard users commonly navigate with Tab and Shift+Tab, activate buttons with Enter or Space, and use arrow keys in widgets that follow established interaction patterns. The focus order should follow the interface's logical reading and interaction sequence.
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 3px;
}
The :focus-visible pseudo-class provides a visible focus indicator when the browser determines that it is appropriate, commonly during keyboard navigation. Never remove focus indicators without providing an equally clear alternative.
Avoid positive tabIndex values because they can create confusing focus sequences. Prefer native controls, and use tabIndex={0} only when a custom element genuinely needs to participate in sequential keyboard navigation.
Build Accessible Forms
Form fields need labels that identify the information users should enter. In JSX, associate a label with its control using htmlFor and the input's id. Placeholder text can provide an example, but it should not replace a persistent label.
import { useState } from 'react';
function EmailForm() {
const [email, setEmail] = useState('');
function handleSubmit(event) {
event.preventDefault();
console.log('Submitted email:', email);
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email address</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
value={email}
onChange={event => setEmail(event.target.value)}
required
/>
<button type="submit">Continue</button>
</form>
);
}
The associated label provides the input's accessible name. The email input communicates the expected data type, and the required attribute identifies a required field. Server-side validation is still necessary because client-side validation can be bypassed.
Communicate Validation Errors
When a field contains invalid data, explain the problem and how to correct it. Use aria-invalid to communicate the field's current invalid state and aria-describedby to associate the input with its error message.
function EmailField({ email, error, onChange }) {
return (
<div>
<label htmlFor="email">Email address</label>
<input
id="email"
name="email"
type="email"
value={email}
onChange={onChange}
aria-invalid={error ? true : undefined}
aria-describedby={error ? 'email-error' : undefined}
/>
{error && (
<p id="email-error">{error}</p>
)}
</div>
);
}
The error message is associated with the input only while an error exists. For larger forms, consider displaying a summary of submission errors and providing a clear path to the first invalid field. Avoid announcing the same error repeatedly without a meaningful reason.
Understand ARIA Attributes
Accessible Rich Internet Applications (ARIA) attributes communicate names, roles, states, and relationships that native HTML may not fully express for custom interfaces. Common attributes include aria-label, aria-labelledby, aria-describedby, aria-expanded, and aria-live.
import { useState } from 'react';
function NavigationMenu() {
const [open, setOpen] = useState(false);
return (
<div>
<button
type="button"
aria-expanded={open}
aria-controls="navigation-links"
onClick={() => setOpen(current => !current)}
>
Navigation
</button>
<nav id="navigation-links" hidden={!open}>
<a href="/documentation/react-introduction">
React introduction
</a>
<a href="/documentation/react-hooks">
React Hooks
</a>
</nav>
</div>
);
}
The expanded state communicates whether the menu is open, while the controls relationship identifies the controlled element. The native button already provides keyboard activation, so custom key handlers are unnecessary for this simple toggle.
ARIA does not automatically implement interaction behavior. Adding role="button" to a generic element does not make it keyboard-operable or focusable by itself. Use native elements first and add ARIA only when it communicates necessary information.
Provide Meaningful Image Alternatives
Informative images need alternative text that communicates their relevant meaning. Decorative images should generally use an empty alt attribute so assistive technologies can ignore them.
function ProductImages() {
return (
<section>
<img
src="/images/keyboard.jpg"
alt="Compact mechanical keyboard with white keycaps"
/>
<img
src="/images/decorative-divider.svg"
alt=""
/>
</section>
);
}
Describe what matters in the context of the page instead of repeating a filename. If an image contains essential text or information, ensure the same information is available in an accessible form.
For decorative icons inside an already labeled control, avoid announcing the icon separately when it adds no meaningful information. An icon-only button should still have an accessible name that identifies its action.
Manage Focus During React Updates
Conditional rendering can open a search panel, display a dialog, remove an element, or replace a section of the page. Focus should remain predictable when these changes occur. Rendering a new element does not automatically move keyboard focus to it.
import { useEffect, useRef, useState } from 'react';
function SearchPanel() {
const [open, setOpen] = useState(false);
const inputRef = useRef(null);
useEffect(() => {
if (open) {
inputRef.current?.focus();
}
}, [open]);
return (
<section>
<button type="button" onClick={() => setOpen(true)}>
Open search
</button>
{open && (
<div>
<label htmlFor="site-search">Search documentation</label>
<input
ref={inputRef}
id="site-search"
type="search"
/>
</div>
)}
</section>
);
}
The Effect moves focus to the input after it has been rendered. In a production interface, also consider how focus should return when the panel closes and whether moving focus is appropriate for the user's task. For a modal, implement the complete dialog interaction pattern rather than only focusing the first field.
Build Accessible Dialogs
A modal dialog temporarily places a user in a focused interaction. It should expose a meaningful name, move focus inside when opened, keep keyboard interaction within the modal while it is active, and return focus to the invoking control when it closes.
function ConfirmationContent({ onCancel, onConfirm }) {
return (
<section aria-labelledby="confirm-title">
<h2 id="confirm-title">Confirm changes</h2>
<p>Do you want to continue?</p>
<button type="button" onClick={onCancel}>
Cancel
</button>
<button type="button" onClick={onConfirm}>
Confirm
</button>
</section>
);
}
This example shows the content and naming structure, not a complete modal implementation. A production modal needs an appropriate dialog element or role, correct opening and closing behavior, focus management, and a way to prevent interaction with background content while it is active. Native dialog features or a well-tested accessible component library can help meet these requirements.
Announce Dynamic Status Messages
React can update content without loading a new page. Screen reader users may not automatically notice changes elsewhere in the document, so important status updates can be communicated through live regions.
function SaveStatus({ saved }) {
return (
<p role="status">
{saved ? 'Changes saved successfully.' : ''}
</p>
);
}
The status role is intended for non-urgent updates that can generally be announced politely. Use a suitable strategy for errors that require immediate attention, and avoid announcing every minor render or rapidly changing value because excessive announcements can overwhelm users.
Maintain Sufficient Color Contrast
Text and meaningful interface elements must remain distinguishable from their backgrounds. Low contrast can make content difficult to read, especially for users with low vision or color vision deficiencies.
- Choose text and background colors with sufficient contrast.
- Do not use color as the only way to communicate errors or success.
- Keep text readable when users zoom the page.
- Provide visible borders, focus indicators, or other cues for interactive elements.
- Respect reduced-motion preferences for animations and transitions.
WCAG 2.2 Level AA generally requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text, subject to defined exceptions. Non-text controls and meaningful graphical objects have separate requirements. Use a contrast checker to verify actual colors instead of relying on visual judgment alone.
Respect Reduced Motion
Animations can provide helpful feedback, but excessive motion may distract users or cause discomfort. The prefers-reduced-motion media query lets CSS adapt to the user's operating-system preference.
.animated-panel {
transition: transform 250ms ease;
}
@media (prefers-reduced-motion: reduce) {
.animated-panel {
transition: none;
animation: none;
}
}
For animations controlled by JavaScript, use the same preference to disable or simplify motion when appropriate. Important state changes should remain understandable even when animations are removed.
Create Accessible Custom Widgets
Custom dropdowns, tabs, accordions, menus, and dialogs require careful attention to semantics and interaction. Their accessibility depends on correct roles, accessible names, keyboard behavior, focus management, and state communication.
Before implementing a custom widget, check whether native HTML elements meet the requirement. If a custom pattern is necessary, follow the relevant WAI-ARIA Authoring Practices guidance and test with keyboard navigation and screen readers. Adding ARIA attributes without implementing the expected behavior can make a widget less usable.
Test React Accessibility
Accessibility testing should combine automated tools with manual evaluation. Automated checks can identify many common issues, but they cannot determine whether every workflow is understandable or whether the interface works well with all assistive technologies.
- Keyboard testing: Navigate workflows using Tab, Shift+Tab, Enter, Space, and appropriate arrow-key interactions.
- Screen reader testing: Verify names, headings, form errors, status announcements, and dialog behavior.
- Automated audits: Use axe-core and browser accessibility tools to detect common violations.
- Component tests: Verify accessible roles, labels, keyboard interactions, and exposed states.
- Visual checks: Test contrast, zoom, responsive layouts, and focus indicators.
- Workflow testing: Test complete tasks such as submitting forms, opening menus, and closing dialogs.
Testing with React Testing Library
React Testing Library encourages tests that interact with components through the same kinds of roles and accessible names users rely on. This can reveal problems that implementation-specific selectors may overlook.
import { useState } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest';
function ToggleButton() {
const [enabled, setEnabled] = useState(false);
return (
<button
type="button"
aria-pressed={enabled}
onClick={() => setEnabled(current => !current)}
>
Notifications
</button>
);
}
test('toggles notification state', async () => {
const user = userEvent.setup();
render(<ToggleButton />);
const button = screen.getByRole('button', {
name: 'Notifications'
});
expect(button).toHaveAttribute('aria-pressed', 'false');
await user.click(button);
expect(button).toHaveAttribute('aria-pressed', 'true');
});
The test queries the button by its role and accessible name, then checks its exposed pressed state before and after interaction. The example assumes the project has React Testing Library, user-event, Vitest, and the required DOM matcher configuration installed. Automated component tests supplement rather than replace manual accessibility testing.
Learn more in React Testing.
Common Accessibility Mistakes
- Using clickable div elements: Prefer native buttons and links with built-in interaction behavior.
- Leaving inputs unlabeled: Associate a visible label with every form control that needs one.
- Using placeholders as the only label: Provide a persistent accessible name.
- Removing focus indicators: Preserve a clear visual indication of keyboard focus.
- Adding ARIA without behavior: Ensure roles, states, and properties reflect actual interaction.
- Ignoring focus after updates: Manage focus when dialogs open, content disappears, or workflows change context.
- Relying only on color: Provide additional cues for errors, success, and important states.
- Testing only with a mouse: Include keyboard and assistive technology testing in development.
Accessibility Best Practices
- Start with semantic HTML and native controls.
- Give form fields meaningful, programmatically associated labels.
- Maintain logical keyboard navigation and visible focus indicators.
- Use ARIA only when native semantics are insufficient.
- Provide meaningful alternative text for informative images and empty alt text for decorative images.
- Associate validation messages with their fields and communicate errors clearly.
- Manage focus deliberately when opening and closing dialogs or changing major interface states.
- Check contrast and respect reduced-motion preferences.
- Test with automated tools, keyboard-only navigation, and screen readers.
- Review WCAG requirements throughout development rather than waiting until release.
Related React Topics
- React JSX — write markup and expressions inside React components.
- React Components — create reusable interface elements.
- React Events — handle user interactions and keyboard events.
- React Forms — implement inputs, validation, and submission.
- React State — manage interactive component states.
- React Styling — style responsive interfaces and focus states.
- React Testing — verify component behavior and accessible interactions.