React
Building Accessible Components in React Without Slowing Down
Practical patterns for keyboard support, focus management, and ARIA in React—without over-engineering.
Accessible React components don’t require a different architecture—they need the right elements, keyboard behavior, and ARIA where semantics aren’t enough. Here’s how to build them without slowing delivery.
Start with the right elements
Use semantic HTML first. Buttons for actions, links for navigation, headings for structure. Use <button> for toggles and dialogs, not <div onClick>. That gives you focus, keyboard activation, and screen reader behavior for free. Reserve divs and spans for layout and styling when no semantic element fits.
// Prefer
<button type="button" onClick={handleOpen} aria-expanded={isOpen}>
Open menu
</button>
// Avoid
<div role="button" tabIndex={0} onClick={handleOpen}>
Open menu
</div>When you must use a non-semantic element (e.g. a custom card that acts as a link), add role, tabIndex, and keyboard handlers and ensure focus is visible.
Keyboard support for custom widgets
Custom dropdowns, modals, and tabs must be keyboard usable. Handle Enter and Space for activation, Arrow keys for movement where appropriate, and Escape to close. Manage focus: move focus into the widget when it opens (e.g. first focusable item or the trigger) and trap focus inside modals until they close. When a modal closes, return focus to the element that opened it.
function Dialog({ open, onClose, children }) {
const ref = useRef(null);
useEffect(() => {
if (!open) return;
const previouslyFocused = document.activeElement;
ref.current?.focus();
const handleKey = (e) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", handleKey);
return () => {
document.removeEventListener("keydown", handleKey);
previouslyFocused?.focus();
};
}, [open, onClose]);
if (!open) return null;
return (
<div
ref={ref}
role="dialog"
aria-modal="true"
tabIndex={-1}
>
{children}
</div>
);
}Reuse a well-tested primitive (e.g. Radix, Reach, Ariakit) when you can; they handle focus trap, arrows, and ARIA. If you build from scratch, mirror their behavior.
ARIA when semantics aren’t enough
Use ARIA to describe state and relationships that HTML doesn’t convey. Examples: aria-expanded for expand/collapse, aria-selected for tabs or listbox options, aria-describedby for hints and errors, aria-live for dynamic announcements. Don’t override semantics: prefer role="button" on a button only when the control isn’t really a button. Test with a screen reader (NVDA, VoiceOver) and keyboard-only; that will surface missing labels and focus issues.
Don’t block the main thread
Accessibility isn’t only semantics. If the UI is laggy, keyboard and screen reader users suffer too. Keep heavy work off the main thread (workers, chunking), virtualize long lists, and avoid layout thrash. Focus management and ARIA are quick to add; performance is a prerequisite for a good experience.
Summary
Use semantic elements, add keyboard support and focus management for custom widgets, and use ARIA for state and relationships. Prefer robust primitives when possible; otherwise implement the same patterns. Combine that with solid performance and you’ll ship accessible components without slowing down.