30% offEnding soon
useFocusTrapLoading saved progress…

useFocusTrap

A focus trap keeps keyboard focus inside a container while it is open: pressing Tab past the last control loops back to the first instead of escaping to the rest of the page, and closing the trap returns focus to wherever it started. It is the piece of accessibility that makes a modal usable without a mouse — the WAI-ARIA dialog pattern requires it. Build useFocusTrap(active), a hook that returns a ref you attach to the container; while active is true, keyboard focus is confined inside it. Unlike useFocus, which focuses a single element on demand, this confines Tab across a whole subtree and restores focus on release.

Signature

function useFocusTrap(active: boolean): React.RefObject<HTMLElement>;
// attach the returned ref to the container you want to trap focus within

When active becomes true the hook moves focus into the container and traps Tab there; when it becomes false (or the component unmounts) it returns focus to the element that had it before.

Examples

function Dialog({ open, onClose }) {
  const ref = useFocusTrap(open);
  if (!open) return null;
  return (
    <div ref={ref} role="dialog" aria-modal="true">
      <button onClick={onClose}>Cancel</button>
      <button onClick={onClose}>Save</button>
    </div>
  );
}
// Opening focuses Cancel; Tab from Save wraps to Cancel; closing refocuses
// the button that opened the dialog.
// The tabbable set is computed live, skipping untabbable nodes:
//   <button>Close</button>           → tabbable (first)
//   <input disabled />               → skipped
//   <span tabindex="-1">…</span>     → skipped
//   <button>Save</button>            → tabbable (last)
// Tab from "Save" wraps to "Close"; Shift+Tab from "Close" wraps to "Save".

Notes

  • active drives everything. True moves focus in and traps Tab; false (or unmount) removes the trap and restores the previously focused element. A false-from-the-start trap does nothing.
  • Tab wraps only at the edges. Tab from the last focusable goes to the first; Shift+Tab from the first goes to the last. A Tab in the middle is the browser's to handle — do not intercept it.
  • Empty container focuses itself. If there are no focusable descendants, move focus to the container so it still cannot rest on the page behind.
  • Skip the untabbable. Filter out disabled and tabindex="-1" elements. Filtering hidden elements (display:none, visibility:hidden, inert) needs layout measurement and is out of scope here.
  • Recompute at Tab time. The container's contents can change while open, so read the focusable set on each Tab, not once on activation.