Skip to content

useFocusTrap

standalonefocusaccessibility

Trap Tab/Shift+Tab focus within a container element. Auto-focuses first focusable element on mount, restores focus on unmount.

useFocusTrap keeps Tab and Shift+Tab focus inside a container while it is mounted — the standard requirement for modal dialogs. It is standalone (no provider), moves focus into the container on activation, recaptures focus if it escapes, and restores focus to the previously focused element when the trap releases. Nested traps stack: only the topmost trap captures, and releasing it re-arms the one beneath.

tsx
useFocusTrap(containerRef)

When to use it

  • Modal dialogs, confirmation prompts, and other surfaces that must hold focus until dismissed.
  • Any overlay where Tab should never reach the page behind it.

A focus trap governs Tab order; it does not close on Escape or lock scroll. Pair it with useKey for Escape-to-close and useScrollLock for background scroll. For focus restoration around triggerless UI without trapping, see useFocusRestore.

Installation

$pnpm exec dgadd add keys/focus-trap
[Installs to]src/hooks/use-focus-trap.ts[Item]keys/focus-trap

dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.

@diffgazer/keys requires no CSS or Tailwind setup.

Parameters

NameTypeDefaultDescription
containerRefrequiredRefObject<HTMLElement | null>Ref to the container element that traps focus. Passed as the first positional argument.
options.initialFocusRefObject<HTMLElement | null>Ref to the element that receives focus when the trap activates. Defaults to the first focusable element.
options.restoreFocusbooleantrueRestore focus to the previously focused element when the trap deactivates.
options.enabledbooleantrueWhether the focus trap is active.

The container ref is the first positional argument; options follow as the second argument.

Returns

voidThis hook does not return a value.

Keyboard behavior

KeyBehavior
TabAdvances through tabbable elements; wraps from the last back to the first.
Shift+TabMoves backward; wraps from the first to the last.
Tab (no tabbables)Prevented; focus stays on the container so the trap is never escaped.

Focusable and tabbable are treated as distinct: programmatic recapture may target a tabIndex={-1} container, while Tab cycling only visits tabbable elements. Focus detection and Tab order follow the composed tree. A trap can be rendered inside an open shadow root or contain nested open shadow roots; their tabbable descendants participate in the same order as light-DOM controls. A MutationObserver re-captures focus if the focused element is removed or becomes unfocusable.

Examples

Modal focus trap

Preview

Custom initial focus

Preview

Notes

Standalone

useFocusTrap does not require KeyboardProvider. It intercepts Tab/Shift+Tab with a capture-phase keydown listener on the container's document while the trap is active, and uses a capture-phase focusin listener there to recapture escaped focus.

Tab wrapping

When focus reaches the last focusable element, Tab wraps to the first. Shift+Tab from the first wraps to the last.

Edge cases

  • initialFocus. Defaults to the first focusable element. Provide a ref to focus a specific control — for a destructive prompt, point it at Cancel rather than Confirm.
  • restoreFocus. On by default; focus returns to the element that was focused before the trap activated. When an outer trap is still active, focus returns to that trap instead of the pre-trap element.
  • Container tabindex. If the container has no tabindex, the hook adds tabindex="-1" while active and removes it on release so an empty trap can still hold focus.
  • Open shadow roots. Containers rendered inside an open shadow root and containers with nested open shadow roots use the same initial focus, composed Tab order, escape recapture, and restoration behavior as light-DOM containers. Closed shadow roots remain opaque.

Source

Highlighted source loads after this disclosure opens. Browse the source repository.