useFocusTrap
Events & gesturesTrap keyboard focus inside a subtree — the accessibility primitive behind modals, dialogs, drawers, and popovers
Install
$ npm install @usefy/use-focus-trapQuick start
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.
activedefaults totrue, 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.
| Parameter | Type | Default | Description |
|---|---|---|---|
active | boolean | true | Whether the trap is active. When it flips false, the listener is removed and focus is restored. |
options | UseFocusTrapOptions | {} | See below. |
Options — UseFocusTrapOptions
| Option | Type | Default | Description |
|---|---|---|---|
initialFocus | FocusTarget | false | first focusable | Where focus lands on activation. An element, ref, or getter. false means don't move focus (you place it yourself). |
returnFocus | boolean | FocusTarget | true | Where focus returns on deactivation/unmount. true restores the element focused at activation; a target overrides it; false skips restore. |
onEscape | (event: KeyboardEvent) => void | — | Called 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 positivetabindex), reusable for roving-tabindex and custom keyboard navigation. Controls disabled directly or by an ancestor<fieldset disabled>, as well ashidden/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.