All hooks / Events and input
useFocusTrap
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| Parameter | Description |
|---|---|
active | Trap focus while true. Default true. |
Returns A callback ref. Pass it to the container's ref prop.
Browser support
| Uses | Chrome | Edge | Firefox | Safari | Chrome Android | Safari iOS |
|---|---|---|---|---|---|---|
| checkVisibilityoptionalWidely available | 105+ | 105+ | 106+ | 17.4+ | 105+ | 17.4+ |
| inertWidely available | 102+ | 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).