Focus Interactions
Headless Focus Interactions; covers React Aria useFocus, React Aria useFocusRing, WICG focus-visible.
| Package | @vizejs/ui/focus |
| Maturity | stable |
| Own the source | vize lib pull focus |
| Requires | interaction-modality |
| Aliases | focus ring, focus within, focus visible |
| Covers | React Aria useFocus, React Aria useFocusRing, WICG focus-visible |
API
createFocus
Create an SSR-safe observer for direct or composed-descendant focus.
function createFocus(options: FocusOptions = {}): FocusController
createFocusWithin
Observe composed-descendant focus ownership.
function createFocusWithin(options: FocusWithinOptions = {}): FocusController
createFocusRing
Create direct focus and focus-visible state for a visible ring.
function createFocusRing(options: FocusRingOptions = {}): FocusRingController
useFocus
Create a direct focus observer disposed with the current Vue effect scope.
function useFocus(options: FocusOptions = {}): FocusController
useFocusWithin
Create a focus-within observer disposed with the current Vue effect scope.
function useFocusWithin(options: FocusWithinOptions = {}): FocusController
useFocusRing
Create focus-ring state disposed with the current Vue effect scope.
function useFocusRing(options: FocusRingOptions = {}): FocusRingController
capture
function capture(errors: unknown[], callback: () => void): void
surfaceErrors
function surfaceErrors(errors: readonly unknown[], message: string): void
readBoolean
function readBoolean(value: FocusOptions["isDisabled"], name: string): boolean
validateBoolean
function validateBoolean(resolved: unknown, name: string): boolean
readMode
function readMode(value: FocusMode | undefined): FocusMode
validateOptions
function validateOptions(options: FocusOptions): FocusMode
eventElement
function eventElement(value: EventTarget | null): Element | null
validateElement
function validateElement(value: unknown): Element
composedContains
Test composed ancestry without assuming that both nodes share one realm.
function composedContains(host: Element, candidate: Element | null): boolean
activeElementOf
Resolve the deepest active element through open shadow roots.
function activeElementOf(host: Element): Element | null
ownsFocus
function ownsFocus(mode: FocusMode, host: Element, active: Element | null): boolean
createFocusEvent
function createFocusEvent( type: FocusEvent["type"], mode: FocusMode, host: Element, focusedTarget: Element | null, relatedTarget: Element | null, originalEvent: globalThis.FocusEvent | null, isFocusVisible: boolean, reason: FocusChangeReason, ): FocusEvent
Behavior
Normative state × input → outcome table for @vizejs/ui/focus. Every row is
exercised by src/families/accessibility/focus/focus*.test.ts; compile-only
assertions live in src/families/accessibility/focus/focus.types.test-d.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| F1 | idle target | host receives focus | immutable focus snapshot and owned state are published | direct-focus test |
| F2 | idle target | descendant receives focus | descendant focus is ignored | ownership-mode test |
| F3 | idle within | composed descendant receives focus | host owns focus and records the deepest active element | within/shadow tests |
| F4 | focused within | focus moves between composed descendants | ownership remains active without duplicate transitions | within-boundary test |
| F5 | focused | focus leaves the owned boundary | immutable blur snapshot includes the destination | direct/within tests |
| F6 | focused | modality changes from pointer to keyboard | focus-visible state updates without a duplicate focus phase | modality test |
| F7 | focused after mount | autoFocus is enabled |
visible-ring state remains true regardless of input modality | auto-focus test |
| F8 | focused | reactive disabled becomes true | lifecycle settles without moving DOM focus | reactive test |
| F9 | focused | reactive disabled resolves to an invalid value | lifecycle settles before the stable runtime diagnostic surfaces | invalid-reactive test |
| F10 | DOM focused, observer idle | explicit refresh | ownership and ring state reconcile from the composed active element | refresh test |
| F11 | focused | manual cancel | listeners/state release while DOM focus remains unchanged | cancellation test |
| F12 | focused | host is removed without blur | mutation observer settles leaked ownership | removal test |
| F13 | focused | host blur delivery is unavailable | document focus safety net settles ownership | safety-net test |
| F14 | transition callback active | callback cancels reentrantly | true→false is ordered and no stale focus phase is published | reentrancy test |
| F15 | callbacks throw | multiple independent callbacks fail | state remains settled and failures aggregate after notification | callback-failure test |
| F16 | listener setup fails | mutation observation cannot start | installed document/modality resources roll back | setup-failure test |
| F17 | cleanup throws | one or more owned resources fail to release | every cleanup runs and all failures remain inspectable | cleanup tests |
| F18 | effect scope ends | any focus hook is scope-owned | controller is terminally disposed without user callbacks | scope test |
| F19 | concurrent SSR requests | identical component trees | byte-identical handler-free markup renders without a server document | SSR test |
| F20 | SSR followed by hydration | host receives and loses focus | DOM identity remains and reactive state renders without diagnostics | hydration test |
| F21 | DOM, SSR, and Vapor lanes | authored consumer compiles | all renderer lanes accept the same public props and reactive state | renderer-conformance gate |
| F22 | root and subpath consumers | only focus is retained | equivalent CSS-free bundles exclude unrelated component signatures | tree-shaking gate |
| F23 | public TypeScript API | state mutation, invalid mode, or callback mismatch | compile-only assertions reject misuse | src/families/accessibility/focus/focus.types.test-d.ts |
Accessibility obligation
The controller normalizes ownership and focus-indicator intent; it never moves
focus, invents a role, or emits CSS. Consumers must bind the returned props to
the same semantic host, keep keyboard focus order meaningful, and render a
visible indicator whenever isFocusVisible is true. autoFocus is reserved
for programmatic mount focus where a ring is required even without keyboard
modality. Focus styling remains entirely user-controlled.