Skip to content
usefy

useFocusTrap

Events & gestures

Trap keyboard focus inside a subtree — the accessibility primitive behind modals, dialogs, drawers, and popovers

Install

$ npm install @usefy/use-focus-trap

Quick start

useFocusTrap.tsx
import { useState } from "react";
import { useFocusTrap } from "@usefy/use-focus-trap";

function Dialog() {
  const [open, setOpen] = useState(false);
  // Focus is trapped while `open`, moved to the first field on open, and
  // restored to the trigger on close. Escape is surfaced so you own the state.
  const ref = useFocusTrap<HTMLDivElement>(open, {
    onEscape: () => setOpen(false),
  });

  return (
    <>
      <button onClick={() => setOpen(true)}>Open dialog</button>
      {open && (
        <div ref={ref} role="dialog" aria-modal="true" aria-label="Sign up">
          <input placeholder="Name" />
          <input placeholder="Email" />
          <button onClick={() => setOpen(false)}>Cancel</button>
          <button onClick={() => setOpen(false)}>Save</button>
        </div>
      )}
    </>
  );
}

API reference

useFocusTrap(active?, options?)

const ref = useFocusTrap<HTMLDivElement>(active, options);
// <div ref={ref}> …focusable content… </div>

Returns a stable callback ref to attach to the container that should contain focus.

Modal-grade "hard" trap. active defaults to true, and while active a <kbd>Tab</kbd> pressed anywhere on the page — even from outside the container — is pulled back into the trap. If you render two traps at once (a dialog opened over another dialog), only the most-recently activated ("topmost") one reacts to <kbd>Tab</kbd> and <kbd>Escape</kbd>; the ones beneath stay dormant until it deactivates, so <kbd>Escape</kbd> fires once and the two Tab handlers never fight.

ParameterTypeDefaultDescription
activebooleantrueWhether the trap is active. When it flips false, the listener is removed and focus is restored.
optionsUseFocusTrapOptions{}See below.

Options — UseFocusTrapOptions

OptionTypeDefaultDescription
initialFocusFocusTarget | falsefirst focusableWhere focus lands on activation. An element, ref, or getter. false means don't move focus (you place it yourself).
returnFocusboolean | FocusTargettrueWhere focus returns on deactivation/unmount. true restores the element focused at activation; a target overrides it; false skips restore.
onEscape(event: KeyboardEvent) => voidCalled when <kbd>Escape</kbd> is pressed inside the trap. The hook never manages open/close state itself.

FocusTarget is HTMLElement | RefObject<HTMLElement | null> | (() => HTMLElement | null) — resolved at the moment focus moves.

Also exported

  • getFocusableElements(container) — the live focusable-descendants helper (in DOM order; it does not reorder for positive tabindex), reusable for roving-tabindex and custom keyboard navigation. Controls disabled directly or by an ancestor <fieldset disabled>, as well as hidden/inert/tabindex="-1"/invisible elements, are excluded.
  • Types: UseFocusTrapOptions, UseFocusTrapRef, FocusTarget.

Go deeper

This page is the quick reference. For every example, prop, and edge case, read the full README — or open Storybook to change props live.

More in events & gestures