Focus utilities
Focus utilities
Section titled “Focus utilities”Dialog and popover contexts already stabilize focus, but dedicated helpers live in src/patterns/focus when you need finer control. The attributes and helpers work declaratively by default and expose initFocusInitial/initFocusMap for imperative workflows.
Initial focus (data-automagica11y-focus-initial)
Section titled “Initial focus (data-automagica11y-focus-initial)”Mark the surface that should receive focus once after hydration. The helper queues the work with queueMicrotask, respects preventScroll, restores the original tabindex after blur, and never clobbers the natural tab sequence for other elements.
| Attribute | Type | Allowed values | Default | Description |
|---|---|---|---|---|
data-automagica11y-focus-initial | boolean attribute | Present on any focusable element | N/A | Opt-in marker that tells the helper which element to focus. |
data-automagica11y-focus-delay | number (ms) | 0+ | 0 | Delay before focusing. Useful when waiting for transitions. |
data-automagica11y-focus-prevent-scroll | boolean | `true`, `false` | `true` | Control whether focusing should avoid scrolling the page. |
Example
Section titled “Example”<div hidden id="sheet"> <button data-automagica11y-focus-initial data-automagica11y-focus-delay="150" data-automagica11y-focus-prevent-scroll="false"> Jump here after the sheet animates </button></div>Call initFocusInitial(button) when the sheet mounts to transfer focus after the animation delay. The helper focuses once per element, so repeated openings respect the original tab order when the node blurs.
Focus map (data-automagica11y-focus-map)
Section titled “Focus map (data-automagica11y-focus-map)”Declare a precise tab sequence using selectors instead of raw tabindex. The helper resolves selectors inside the configured scope, wires keyboard handlers that honor Tab and Shift+Tab, and keeps focus anchored to the surrounding DOM without trapping or looping.
The mapped list activates whenever the attribute resolves to elements and a focus anchor is available. The anchor represents the entry point in the DOM, so pressing Tab on it jumps into the ordered list while Shift+Tab from the first mapped item routes back to it. Tabbing past the last mapped element is intentionally left to the browser so the rest of the page remains reachable—there is no artificial loop.
| Attribute | Type | Allowed values | Default | Description |
|---|---|---|---|---|
data-automagica11y-focus-map | string | string[] | Semicolon or JSON array of selectors | None | Ordered selectors describing the preferred focus sequence. |
data-automagica11y-focus-map-scope | `document` | `self` | Selector | Scope tokens | `document` | Restrict where selectors resolve when building the focus list. |
data-automagica11y-focus-map-anchor | Selector | CSS selector | Scope container (if focusable) | Anchor that captures focus before entering the map and receives it when Shift+Tabbing from the first mapped item. Tabbing past the final element continues beyond the map so the rest of the page is unaffected. |
Provide a focusable element with data-automagica11y-focus-map-anchor. If the anchor selector cannot be resolved (and the scope is the document), the focus map stays disabled. The helper temporarily adds tabindex="0" when the anchor lacks natural focusability and restores it on cleanup.
Example
Section titled “Example”<div id="focus-map-anchor" tabindex="0" aria-label="Focus map entry point"></div>
<div id="focus-map-zone" data-automagica11y-focus-map="#navbar button; #player button" data-automagica11y-focus-map-scope="#focus-map-zone" data-automagica11y-focus-map-anchor="#focus-map-anchor"> <div id="navbar"> <button>Playlist</button> <button>Now playing</button> </div> <div id="player"> <button>Play</button> <button>Pause</button> </div></div>
<a href="#outside">The rest of the page</a>Pressing Tab on the anchor jumps into the sequenced buttons, while Tab from the last button naturally lands on the outside link—the focus map does not loop back on itself.
Behavior summary
Section titled “Behavior summary”- The attribute accepts a semicolon-delimited string or JSON array of selectors that resolve against the designated scope (
self, a selector, or the entire document). data-automagica11y-focus-map-anchorties the mapped list back into the natural tab order—Tab from the anchor goes into the list, Shift+Tab from the first element returns to the anchor, and Tab from the last element continues beyond the focus map.- No positive
tabindexvalues are assigned; keyboard order is achieved by intercepting Tab presses and focusing the next/previous mapped element. - When nodes change after initialization, rerun
initFocusMapto refresh the order.
Focus trap (data-automagica11y-focus-trap)
Section titled “Focus trap (data-automagica11y-focus-trap)”Wrap an interactive surface in a managed focus loop so keyboard users never land outside the active UI. The attribute version builds on the shared enableFocusTrap() helper while handling visibility and event-driven activation.
| Attribute | Type | Allowed values | Default | Description |
|---|---|---|---|---|
data-automagica11y-focus-trap | boolean attribute | Present on any container | N/A | Enables the focus trap when the container is visible so Tab/Shift+Tab stay inside. |
data-automagica11y-focus-trap-initial | `first` | `last` | Selector | First, last, or any CSS selector | `first` | Controls where focus moves when the trap activates. |
data-automagica11y-focus-trap-return | boolean | `true`, `false` | `true` | Whether to restore focus to the previously active element when the trap releases. |
data-automagica11y-focus-trap-escape-dismiss | boolean | `true`, `false` | `false` | Opt-in Escape key to release the trap (useful for non-modal surfaces). |
data-automagica11y-focus-trap-auto | boolean | `true`, `false` | `true` | Automatically enable/disable the trap when the container becomes visible or hidden. |
Example
Section titled “Example”<div id="sheet" hidden data-automagica11y-focus-trap data-automagica11y-focus-trap-initial="[data-primary]" data-automagica11y-focus-trap-return="true"> <button data-primary>Primary action</button> <button>Secondary</button> <button data-automagica11y-dialog-close>Cancel</button></div>When the element becomes visible (no hidden attribute and aria-hidden is not true), the trap enables, focuses the first tabbable (or your selector), and keeps Tab/Shift+Tab cycling within. Toggling hidden or removing the element disables the trap and returns focus to the opener unless data-automagica11y-focus-trap-return="false".
Behavior summary
Section titled “Behavior summary”- Auto mode is on by default. Set
data-automagica11y-focus-trap-auto="false"to opt out and toggle traps manually. - The pattern also listens for
automagica11y:toggle,automagica11y:toggle:opened,automagica11y:toggle:closed,automagica11y:shown, andautomagica11y:hiddenevents. Dispatch one withdetail.targetequal to the container to force enable/disable. - Escape dismissal stays opt-in via
data-automagica11y-focus-trap-escape-dismiss="true"so dialogs keep owning their close logic. - Nested traps cooperate—activating a child trap pauses its parent. When the child releases, the parent resumes with its previous focus target.
Per-element links (data-automagica11y-focus-next / data-automagica11y-focus-prev)
Section titled “Per-element links (data-automagica11y-focus-next / data-automagica11y-focus-prev)”Build lightweight relationships between neighboring elements without defining an entire map. Any focusable control can declare a data-automagica11y-focus-next or data-automagica11y-focus-prev selector. When the user presses Tab or Shift+Tab, automagicA11y resolves the selector inside the configured scope, skips hidden/disabled/inert matches, and focuses the first viable result. Reverse edges are inferred automatically, so authoring only the focus-next side still makes backward navigation work.
| Attribute | Type | Allowed values | Default | Description |
|---|---|---|---|---|
data-automagica11y-focus-next | string | Selector | Any CSS selector | None | Points to the element (or its first focusable descendant) that receives focus on Tab. |
data-automagica11y-focus-prev | string | Selector | Any CSS selector | None | Points to the element (or its first focusable descendant) that receives focus on Shift+Tab. |
data-automagica11y-focus-scope | `document` | `self` | Selector | Scope tokens | `document` | Limits where link selectors resolve—use `self` for the current element or supply a selector for a container. |
Example
Section titled “Example”<section class="controls" data-automagica11y-focus-scope="#player-deck"> <button id="shuffle" data-automagica11y-focus-next="#repeat">Shuffle</button> <button id="play" data-automagica11y-focus-next="#queue" data-automagica11y-focus-prev="#shuffle">Play</button> <div id="player-deck"> <button id="repeat" data-automagica11y-focus-prev="#play">Repeat</button> <button id="queue">Queue</button> </div></section>Behavior summary
Section titled “Behavior summary”- Use
data-automagica11y-focus-scope="self"to restrict selectors to the element itself, or pass a selector to limit resolution to a specific container. Without an explicit scope, selectors resolve within the document or shadow root that owns the control. - When the resolved node is not focusable, the helper focuses the first focusable descendant. If no focusable element exists, the browser’s natural tab order continues.
- Focus links are registered once via
initFocusLinks()(called automatically from the bundle) and coexist with focus maps. Combine them to nudge a few controls without maintaining an ordered list.
Shared focus utilities
Section titled “Shared focus utilities”src/core/focus.ts exposes reusable helpers that keep focus changes predictable:
getFocusableIn(root)— finds focusable descendants inside a container.focusElement(element, options)— focuses without permanently alteringtabindex.enableFocusTrap(container, options)— installs a managed focus trap and returns a disposer.applyFocusOrder(elements)— temporarily reorders focus via sequentialtabindexvalues and returns a controller that restores originals.
Coverage plan
Section titled “Coverage plan”- Ship a recipe showing
initFocusInitialpaired with the animate lifecycle so focus waits for exit transitions. - Build a live playground that lets you interactively author focus maps and preview how Tab/Shift+Tab behave.
- Cross-link the pattern documentation from contexts once dialogs, popovers, and future composites highlight which helpers they already call header by default.
Track progress on the Core modules plan as you contribute these additions.