Focus Guards
Headless Focus Guards; covers Radix FocusGuards, React Aria FocusScope sentinels.
| Package | @vizejs/ui/focus-guards |
| Maturity | stable |
| Own the source | vize lib pull focus-guards |
| Requires | focus-scope |
| Aliases | focus sentinels, tab guards, focus trap guards |
| Covers | Radix FocusGuards, React Aria FocusScope sentinels |
API
focusGuardPreset
Optional invisible sentinel preset; consumers remain free to replace every value.
const focusGuardPreset
createFocusGuards
Create an SSR-safe pair of nested, portal-aware focus sentinels.
function createFocusGuards(options: FocusGuardsOptions): FocusGuardsController
useFocusGuards
Create, mount-activate, and scope-dispose a focus-guard pair.
function useFocusGuards(options: FocusGuardsOptions): FocusGuardsController
eventElement
Resolve an event target without relying on the current realm's Element constructor.
function eventElement(value: EventTarget | null): Element | null
readBoolean
function readBoolean(source: unknown, name: string, fallback: boolean): boolean
readRoot
function readRoot(source: unknown): Element | null
readBranches
function readBranches(source: unknown, document: Document | null): readonly Element[]
readTarget
function readTarget(value: unknown): HTMLElement | null
validateOptions
function validateOptions(options: FocusGuardsOptions): void
createRedirectEvent
function createRedirectEvent( position: FocusGuardPosition, direction: FocusGuardDirection, reason: FocusGuardRedirectEvent["reason"], target: HTMLElement | null, relatedTarget: Element | null, originalEvent: globalThis.FocusEvent, ): FocusGuardRedirectEvent
recomputeFocusGuards
function recomputeFocusGuards(document: Document): void
attachFocusGuards
function attachFocusGuards(token: FocusGuardsToken, root: Element): void
detachFocusGuards
function detachFocusGuards(token: FocusGuardsToken): void
Behavior
createFocusGuards supplies consumer-rendered before and after sentinels for a primary focus
region plus optional portalled branches. It complements focus containment; it does not assign
dialog semantics, inert outside content, or lock scrolling.
| Concern | Contract |
|---|---|
| Direction | A before sentinel enters at the first target and wraps backward to the last; an after sentinel performs the inverse. |
| Evidence | Tab direction and relatedTarget distinguish entry from wrapping; redirects publish immutable, preventable events. |
| Ordering | Positive tabindex values precede normal sequential order across the root, portals, open shadows, and rendered slots. |
| Nesting | Only the latest enabled, connected owner in a document exposes tabindex="0"; releasing it synchronously resumes its parent. |
| Recovery | Document mutations detect disconnected and reinserted roots; refresh() also reconciles imperative changes. |
| Fallback | An owned explicit fallback or a focusable root handles regions without a remaining sequential target. |
| Accessibility | Sentinels omit aria-hidden, which must not be placed on focusable content; preventing redirect requires visible focus. |
| Styling | No CSS is emitted. A frozen invisible inline-style preset and data attributes are optional consumer hooks. |
| Server rendering | Nullable roots render deterministic inactive sentinels and activate only after hydration mount. |
| Vapor | A public composable fixture must compile without diagnostics in native DOM, SSR, and Vapor lanes. |
| Tree shaking | Root and subpath consumers emit identical JavaScript, retain no unrelated families, and emit zero CSS. |
Place the sentinels immediately before and after the rendered region in sequential DOM order. Portalled branches participate in destination discovery even though their DOM location is independent.
The redirect contract follows the HTML sequential focus navigation model and the WAI-ARIA modal dialog requirement that Tab and Shift+Tab remain within the active dialog. Focus order must remain meaningful under WCAG 2.2 Success Criterion 2.4.3.