All hooks / Events and input

useFocusTrap

512 B minified and brotliedSourceMarkdown✓ Works in all modern browsers

Keeps keyboard focus inside an element while active, e.g. a custom modal or drawer. Returns a callback ref for the element.

On activation, focuses the element with autofocus inside, else the first focusable element, else the container itself. Tab and Shift+Tab move through the focusable elements in DOM order and wrap around, the same in every browser (Safari otherwise skips buttons). If focus moves outside (e.g. a click elsewhere), it is brought back. On deactivation or unmount, focus returns to the element that had it before, usually the button that opened the dialog.

Import

import { useFocusTrap } from '@vincent.ri/react-tils'
// or only this hook
import { useFocusTrap } from '@vincent.ri/react-tils/useFocusTrap'

Usage

function Drawer({ open, onClose }: { open: boolean; onClose: () => void }) {
  const ref = useFocusTrap<HTMLDivElement>(open)
  useHotkey('Escape', onClose, { enabled: open })
  if (!open) return null
  return (
    <div ref={ref} role="dialog" aria-modal="true" aria-label="Menu">
      <button onClick={onClose}>Close</button>
      <nav>…</nav>
    </div>
  )
}

Live demo

Open, then press Tab: focus stays inside.

When to use it

A custom modal, drawer, or full-screen menu that you can't build with <dialog>.

When not to

You can use <dialog> with showModal(); it traps focus, restores it, closes on Escape, and makes the rest of the page inert, with no JS. A focus trap alone doesn't hide the page from screen readers: also set aria-modal="true" on the dialog, or inert on the rest of the page.

API

function useFocusTrap<T extends HTMLElement = HTMLElement>(active?: boolean): (element: T | null) => void
ParameterDescription
activeTrap focus while true. Default true.

Returns A callback ref. Pass it to the container's ref prop.

Browser support

UsesChromeEdgeFirefoxSafariChrome AndroidSafari iOS
checkVisibilityoptionalWidely available105+105+106+17.4+105+17.4+
inertWidely available102+102+112+15.5+102+15.5+

Optional features are used when present; without them the hook still works.

First version with support, from MDN's compatibility data (web-features 3.42.0, @mdn/browser-compat-data 8.1.5).