Skip to content

Modern React 36: Accessibility as component API design

This is part 36 of the Modern React development series.

Accessible React is mostly accessible HTML with good component contracts. A component API should make the accessible path the natural path by asking for labels, relationships, state, and semantics up front.

Concept

Accessibility is the practice of making UI usable by people with different input methods, assistive technologies, vision, hearing, motion, and cognitive needs. In React components, accessibility often shows up as semantic elements, labels, keyboard behavior, focus management, and ARIA when native HTML is not enough.

Terms

  • ARIA: Accessible Rich Internet Applications, attributes that add accessibility semantics when native HTML cannot express them.
  • Accessible name: The name assistive technologies use for a control, often from text, aria-label, or a label element.
  • Focus management: Controlling where keyboard focus moves after an interaction.
  • Semantic HTML: Using elements such as button, nav, label, and section for their built-in meaning.

Mental model

Think of accessibility as part of the component’s public API. If a caller can render an unlabeled button or disconnected field, the component API allowed an incomplete state.

How it is used

Use this model for buttons, icon buttons, forms, dialogs, menus, tabs, alerts, navigation, table components, and any component that wraps native controls.

How to use it

  1. Start with the native element that matches the interaction.
  2. Require labels or label IDs in the component API when visible text is not enough.
  3. Expose state through native attributes or ARIA attributes.
  4. Preserve keyboard behavior and focus order.
  5. Test with role and label queries so accessibility is exercised during component tests.

Example: Icon button requires a label

import type { ReactNode } from "react";
type IconButtonProps = {
label: string;
icon: ReactNode;
onClick: () => void;
};
export function IconButton({ label, icon, onClick }: IconButtonProps) {
return (
<button type="button" aria-label={label} onClick={onClick}>
{icon}
</button>
);
}
React output

An icon alone usually has no accessible name. Requiring label makes the contract complete.

Example: Field component wires label and error

import { useId } from "react";
type TextFieldProps = {
label: string;
error?: string;
};
export function TextField({ label, error }: TextFieldProps) {
const inputId = useId();
const errorId = useId();
return (
<div>
<label htmlFor={inputId}>{label}</label>
<input
id={inputId}
aria-invalid={error ? true : undefined}
aria-describedby={error ? errorId : undefined}
/>
{error && <p id={errorId}>{error}</p>}
</div>
);
}
React output

Enter a valid email address.

The field component owns the ID wiring so callers cannot forget the label relationship.

Details to watch

  • Native first: A real button carries keyboard and role behavior that a clickable div does not.
  • ARIA role: ARIA augments semantics. It does not add missing interaction behavior by itself.
  • Generated IDs: useId helps connect labels and descriptions without hard-coded duplicate IDs.
  • Testing: Queries by role and label catch many component API accessibility gaps.

Series navigation

References