Skip to main content
The FTC fined the leading overlay provider $1M for deceptive claims. Learn why it matters (opens in new tab)
Developer Guides

Building accessible React components

November 1, 20259 min read

React's component model creates specific accessibility challenges: focus is lost on re-renders, dynamic content updates don't trigger screen reader announcements, and component libraries often paper over accessibility with half-solutions. Here are the patterns that make React applications genuinely accessible.

Managing focus with useRef and useEffect

In React, focus management requires explicit coordination because the DOM changes happen asynchronously. When a dialog opens, use a ref to hold the trigger element and useEffect to move focus after the component mounts. When the dialog closes, call triggerRef.current.focus() to restore focus. useEffect with a dependency on the open state is the correct place to manage these transitions.

Focus management in a dialog component

function Dialog({ open, onClose, children }) {
  const dialogRef = useRef(null);
  const triggerRef = useRef(null);

  useEffect(() => {
    if (open) {
      // Store the element that opened the dialog
      triggerRef.current = document.activeElement;
      // Move focus into the dialog
      dialogRef.current?.focus();
    } else {
      // Restore focus when dialog closes
      triggerRef.current?.focus();
    }
  }, [open]);

  return open ? (
    <div
      ref={dialogRef}
      role="dialog"
      aria-modal="true"
      aria-labelledby="dialog-title"
      tabIndex={-1}
    >
      {children}
    </div>
  ) : null;
}

Announcing dynamic content changes

React state updates can change the DOM without triggering screen reader announcements. The pattern for announcements is a visually hidden live region that React updates when a message needs to be read. Maintain a separate piece of state for the announcement text and inject it into a div with role="status" or role="alert". Clear and reset the message so repeated identical updates still fire.

Accessible toast/announcement hook

function useAnnouncement() {
  const [message, setMessage] = useState('');

  const announce = useCallback((msg) => {
    setMessage('');
    // Timeout ensures state change is detected even for repeated messages
    setTimeout(() => setMessage(msg), 50);
  }, []);

  const AnnouncementRegion = () => (
    <div
      role="status"
      aria-live="polite"
      aria-atomic="true"
      className="sr-only"
    >
      {message}
    </div>
  );

  return { announce, AnnouncementRegion };
}

Component library pitfalls

Popular React component libraries (shadcn/ui, Radix UI, Material UI) vary significantly in their accessibility quality. Radix UI primitives are built on the ARIA APG patterns and are generally reliable. Material UI has good coverage but requires correct prop usage. Many smaller libraries expose accessible-looking APIs that break in practice — e.g., Select components that don't work with screen readers, or Dialog components that don't trap focus. Always test with a screen reader, not just a lint rule.

Tip

Use eslint-plugin-jsx-a11y in your project. It catches a significant portion of static accessibility errors at author time — missing alt text, incorrect ARIA usage, missing form labels, and more. It is not a substitute for runtime testing but dramatically reduces the errors that reach production.

Put it into practice

See exactly where your site stands against WCAG 2.2.

Free scan on any public URL. Full report in under two minutes.