focusScope
elements man ui/behaviors/focus-scopeMoves 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.
Signaturesfunction 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
app/shared/templates/side-panel/index.ehtmlimport { focusScope, type FocusScope } from "@elements/ui/behaviors"; interface SidePanelState { scope?: FocusScope; } <SidePanel (open: boolean = false, private ui: SidePanelState = {})> <aside e:if={open} class="side-panel" role="dialog" aria-modal="true" oninsert={(e) => ui.scope = focusScope(e.target as HTMLElement, { trap: true })} onremove={() => ui.scope?.release()}> <input data-autofocus placeholder="Search"> <slot/> <button type="button" onclick={() => open = false}>Close</button> </aside> </SidePanel>
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 (opens in a new tab) and Popover (opens in a new tab), and so every template built on them: AlertDialog, Sheet, Drawer, HoverCard, Tooltip, Select, Combobox, DatePicker.