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.