# focusScope Moves focus into a container when it opens, optionally traps it there, and puts it back where it was when the container closes. Scopes stack: a trapped scope opened inside another pauses the outer one, and releasing it resumes the outer one, so nested dialogs each keep their own trap. ```ts Signatures function focusScope(container: HTMLElement, opts?: FocusScopeOptions): FocusScope; function focusScopeDepth(): number; type FocusTarget = string | HTMLElement | (() => HTMLElement | null); ``` A `FocusTarget` is a selector matched inside the container, an element, or a function that returns one. ## Options | Option | Type | Default | What it does | |---|---|---|---| | `trap` | `boolean` | `false` | Keep focus inside: Tab wraps at the edges and focus moved outside is pulled back. | | `loop` | `boolean` | same as `trap` | Tab wraps at the edges. | | `initialFocus` | `FocusTarget \| false` | see below | Where focus goes on open. `false` leaves it where it is. | | `returnFocus` | `FocusTarget \| false \| null` | the element focused before the scope opened | Where focus returns on release. `false` skips. | | `inside` | `() => (Element \| null \| undefined)[]` | none | Extra elements that count as inside the trap: a popover rendered outside the container. | Initial focus, in order: `initialFocus`; an `[autofocus]` or `[data-autofocus]` element inside; the first tabbable element that is not a link; the container itself, given `tabindex="-1"`. Tab follows the browser's own stops at the edges: a native radio group is one stop on its checked radio, and controls in a disabled fieldset are not stops. Focus returns on release only when it is still inside the closing container (or an `inside()` element) or was lost to the body; a user who clicked elsewhere keeps their place. ## The scope | Member | What it does | |---|---| | `container` | The element the scope was opened on. | | `release(returnFocus?)` | Stops trapping and returns focus. Pass `false` to skip returning it. Safe to call twice. | | `focusFirst()`, `focusLast()` | Focus the first or last tabbable element. | | `focusNext(opts?)`, `focusPrevious(opts?)` | Focus the element after or before the focused one. | `focusNext` and `focusPrevious` take `FocusManagerOptions`: | Option | Type | Default | What it does | |---|---|---|---| | `from` | `Element` | the focused element | Start from this element instead. | | `tabbable` | `boolean` | `false` | Only elements in the Tab order (tabindex >= 0); otherwise anything focusable. | | `wrap` | `boolean` | `false` | Go round at the ends. | | `accept` | `(el: Element) => boolean` | none | Only elements this returns true for. | ## In a template of your own ```ehtml app/shared/templates/side-panel/index.ehtml import { focusScope, type FocusScope } from "@elements/ui/behaviors"; interface SidePanelState { scope?: FocusScope; } ``` The input takes initial focus through `data-autofocus`, Tab cycles inside the panel, and releasing the scope on removal puts focus back on whatever opened it. ## Writes `tabindex="-1"` on a container that had no tabindex, removed on release. ## Used by [Dialog](../templates/dialog) and [Popover](../templates/popover), and so every template built on them: AlertDialog, Sheet, Drawer, HoverCard, Tooltip, Select, Combobox, DatePicker.