React Accessibility (a11y)
Build inclusive, WCAG-compliant React applications using semantic HTML, ARIA attributes, and keyboard focus management.
TL;DR
- Build inclusive web applications using semantic
HTMLelements. - Enhance assistive technology experiences with accessible
ARIAattributes. - Manage interactive keyboard focus transitions smoothly using persistent component DOM
refs.
Semantic HTML and ARIA
Native Button SemanticsUse native button for keyboard focus and enter key.
// Accessible by default
<ButtonBase onClick={handleAction}>
Submit Form
</ButtonBase>Accessible Icon ButtonSupply text description for icon-only buttons.
<ButtonBase aria-label="Close Navigation">
<CloseIcon />
</ButtonBase>Dynamic ARIA StatesReflect expanded or selected state dynamically.
<ButtonBase
aria-expanded={isOpen}
aria-controls="dropdown-menu"
onClick={toggle}
>
Menu
</ButtonBase>Focus Management
Focus on Mount with RefShift focus to modal or input upon appearance.
const firstInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
firstInputRef.current?.focus();
}, []);Skip Navigation LinkProvide fast skip link for keyboard-only users.
<Link
href="#main-content"
className="sr-only focus:not-sr-only"
>
Skip to Main Content
</Link>Screen Reader Only StyleHide text visually while keeping it accessible.
// Visual text hidden but read by screen readers
<VisuallyHidden>Current step: 2 of 5</VisuallyHidden>Keyboard and Live Regions
Keyboard Handler PatternListen for Enter and Space on custom controls.
const onKey = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onToggle();
}
};Polite Live RegionAnnounce status updates without interrupting speech.
<Alert role="status" aria-live="polite">
{savedStatus}
</Alert>Form Error AssociationLink input error message using aria-describedby.
<InputBase
id="user-email"
aria-invalid={hasError}
aria-describedby="email-error"
/>
{hasError && <Text id="email-error">Invalid</Text>}Form and Field Accessibility
Accessible Fieldset GroupGroup related radio or checkbox inputs with legend.
<FieldsetGroup>
<LegendTitle>Preferences</LegendTitle>
<Checkbox label="Email alerts" />
</FieldsetGroup>Input Label AssociationAssociate form labels explicitly using htmlFor ID.
<LabelText htmlFor="user-name">Name</LabelText>
<InputBase id="user-name" />Error Feedback AnnouncementAnnounce form errors immediately with role alert.
<Text role="alert" aria-live="assertive">
{errorMessage}
</Text>Tips
- Always prefer native HTML elements like
<button>and<dialog>over custom div tags with ARIA role attributes. - Ensure all interactive custom components support Enter, Space, and Escape keyboard navigation with visible
:focusindicators.
Warnings
- Never attach onClick listeners to non-interactive elements like
divorspanwithout providing keyboard event listeners. - Avoid using positive
tabIndexvalues greater than zero because they distort natural document tab order unexpectedly.
In Practice
Build an accessible collapsible disclosure widget with dynamic ARIA attributes, keyboard toggle, and focus outlines.
- Connect trigger button to content panel using aria-controls.
- Reflect expansion state dynamically via aria-expanded.
- Ensure native button handles keyboard space and enter triggers.
- Wrap content in semantic region with appropriate labelling.
export function Accordion({ title, children }: any) {
const [isOpen, setIsOpen] = useState(false);
const panelId = useId();
return (
<CardView>
<ButtonBase
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => setIsOpen((o) => !o)}
>
{title}
</ButtonBase>
{isOpen && (
<CardView id={panelId} role="region">
{children}
</CardView>
)}
</CardView>
);
}FAQ
Native HTML elements like
In JSX, ARIA attributes retain standard hyphenated notation (like aria-expanded and aria-label) rather than camelCase, while maintaining boolean and string values.
A live region is an element with aria-live='polite' or 'assertive' that announces dynamic content changes to screen reader users without requiring manual focus shifts.