React & FrontendAugust 14, 2026•8 min read

Practical Web Accessibility: WAI-ARIA, Keyboard Navigation & Focus Traps in React

Building accessible web interfaces that pass WCAG 2.2 AA audits. Implementing keyboard focus traps, roving tabindex, semantic landmarks, and screen reader announcements without UI compromises.

RB
Rohit Bhatu — Lead Systems Architect
Practical Web Accessibility: WAI-ARIA, Keyboard Navigation & Focus Traps in React

1. The Reality of Modern Accessibility (a11y)

Accessibility is not a cosmetic feature or a regulatory burden—it is core software craftsmanship. Over 15% of global internet users navigate the web using assistive technologies, keyboard-only input, or high-contrast screen magnification.

Search engines directly prioritize websites with semantic, accessible DOM hierarchies, as screen reader parsers and automated web crawlers leverage the exact same underlying accessibility tree.

2. The Native First Rule of ARIA

The first rule of ARIA is: do not use ARIA if a native HTML element already possesses the required semantics and keyboard behaviors.

Writing `<div onClick={handleClick}>Submit</div>` requires manually implementing `role="button"`, `tabIndex={0}`, `onKeyDown` listeners for Enter and Space keys, and disabled state announcements. Replacing it with `<button type="button">Submit</button>` achieves full compliance out of the box with zero boilerplate.

3. Implementing a Bulletproof Keyboard Focus Trap

When a modal or drawer dialog opens, keyboard navigation (`Tab` and `Shift+Tab`) must be constrained strictly within the modal boundary. If focus escapes into hidden background elements, visually impaired users lose spatial context entirely.

Accessible React focus trap hook for dialog overlaystsx
import { useEffect, useRef } from "react";

export function useFocusTrap(isOpen: boolean) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!isOpen || !containerRef.current) return;

    const el = containerRef.current;
    const focusableSelectors = [
      'a[href]',
      'button:not([disabled])',
      'textarea:not([disabled])',
      'input:not([disabled])',
      'select:not([disabled])',
      '[tabindex]:not([tabindex="-1"])',
    ].join(", ");

    const focusableElements = el.querySelectorAll<HTMLElement>(focusableSelectors);
    const firstElement = focusableElements[0];
    const lastElement = focusableElements[focusableElements.length - 1];

    // Initial focus on opening
    firstElement?.focus();

    function handleKeyDown(e: KeyboardEvent) {
      if (e.key === "Tab") {
        if (e.shiftKey) {
          if (document.activeElement === firstElement) {
            e.preventDefault();
            lastElement?.focus();
          }
        } else {
          if (document.activeElement === lastElement) {
            e.preventDefault();
            firstElement?.focus();
          }
        }
      } else if (e.key === "Escape") {
        // Trigger close handler
      }
    }

    el.addEventListener("keydown", handleKeyDown);
    return () => el.removeEventListener("keydown", handleKeyDown);
  }, [isOpen]);

  return containerRef;
}

4. Announcing Dynamic State with aria-live Regions

When an asynchronous action completes (such as submitting our contact form or copying a generated token to clipboard), visual users notice the green checkmark badge instantly.

Screen reader users must be notified programmatically using an `aria-live` region. Use `aria-live="polite"` for non-critical confirmations so the synthesizer finishes reading current text before speaking the update.