NetcaneTechnologies

React

Building Accessible Components in React Without Slowing Down

Practical patterns for keyboard support, focus management, and ARIA in React—without over-engineering.

Yasir Haleem3 min read

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.

About the author

Yasir Haleem is founder and lead engineer at Netcane Technologies. He builds production Next.js sites with headless CMS platforms — Strapi, Contentful, Sanity, and WordPress — with a focus on performance, SEO, and maintainable architecture.

Let's work together

Tell us about your project. We respond within one business day with a clear scope, timeline, and estimate.