FocusScope
FocusScope manages keyboard focus around an overlay. It moves focus in on mount, keeps Tab cycling inside, recaptures focus that escapes, and restores focus on unmount — four behaviours, each one you can switch off.
What it is
A wrapper that owns focus for the subtree inside it. When a modal opens, focus should move into it; while it is open, Tab should not wander out; when it closes, focus should return to whatever opened it. FocusScope does all four, and each behaviour is a separate prop so non-modal uses can take only the parts they need.
Install
FocusScope is exported from usemotif. No separate install.
API
FocusScope
stablefunction FocusScope(props: FocusScopeProps): JSX.Element
Move focus to the first focusable descendant on mount. Falls back to the scope container itself when nothing inside is focusable.
Return focus to the previously-active element on unmount.
Keep Tab and Shift+Tab cycling inside the scope — Tab from the last focusable wraps to the first.
Recapture focus when external code moves it outside the scope — a programmatic `.focus()`, a click on a background element. Defaults to the value of `trapFocus`.
Called when Escape is pressed inside the scope. Wire it to the parent's dismiss handler.
The focus-managed subtree.
Accessibility
The four behaviours together are the WAI-ARIA modal contract. autoFocus means the keyboard user
starts inside the dialog, not stranded behind it. trapFocus means Tab cannot wander to the page
underneath. captureFocus closes the gap trapFocus leaves — a programmatic someElement.focus()
outside the scope is bounced back, so focus cannot escape silently. restoreFocus means closing
the dialog returns the user to where they were.
captureFocus defaults to trapFocus deliberately. A modal traps everything, including
programmatic focus. A non-modal use — focus-restore only, no trap — passes trapFocus={false}
and leaves programmatic focus alone. Wire onEscape to the dismiss handler so Escape closes the
overlay without a separate keydown listener.
Examples
A modal — every behaviour on (the defaults):
Focus restore only — no trap, for a non-modal popover: