Skip to content

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 reference
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.
<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.

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 reference
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.
<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.

  • 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-anchor ties 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 tabindex values are assigned; keyboard order is achieved by intercepting Tab presses and focusing the next/previous mapped element.
  • When nodes change after initialization, rerun initFocusMap to 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 reference
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.
<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".

  • 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, and automagica11y:hidden events. Dispatch one with detail.target equal 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.
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 reference
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.
<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>
  • 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.

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 altering tabindex.
  • enableFocusTrap(container, options) — installs a managed focus trap and returns a disposer.
  • applyFocusOrder(elements) — temporarily reorders focus via sequential tabindex values and returns a controller that restores originals.
  1. Ship a recipe showing initFocusInitial paired with the animate lifecycle so focus waits for exit transitions.
  2. Build a live playground that lets you interactively author focus maps and preview how Tab/Shift+Tab behave.
  3. 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.