Skip to content

API Overview ​

SymbolPurposeExecution modeCommon gotcha
createListNavigation()Build keyboard navigation for composite widgetsSyncApply returned changes to DOM focus
createGridNavigation()Build two-dimensional arrow-key navigation for gridsSyncColumns resolve per navigation: responsive grids need a getter
createQuickLookGrid()Build arrow-key tile browsing with a preview key for card gridsSyncThe preview key consumes Space: the focused button never activates
restoreFocus()Restore focus to a target or fallbackSyncReturns false when neither target can receive focus
rescueFocus()Re-home focus after the focused element unmountsSyncReturns false when focus is already on a real element
captureFocus()Capture active focus for one later restorationSyncThe returned function is one-shot

Package Entry Point ​

ImportPurpose
@vielzeug/focusList navigation and focus restoration primitives.

Core Functions ​

createListNavigation() ​

ts
function createListNavigation<T>(options: ListNavigationOptions<T>): ListNavigation<T>;

Creates a keyboard navigation controller with an internal active index.

ParameterTypeDescription
optionsListNavigationOptions<T>Item lookup, key mapping, navigation, dynamic direction/orientation, and typeahead options.

Returns: ListNavigation<T>.

Example

ts
import { createListNavigation } from '@vielzeug/focus';

const nav = createListNavigation({
  getItems: () => rows,
  isItemDisabled: (item) => item.matches('[aria-disabled="true"]'),
});

const result = nav.handleKeydown(event);
result?.change?.item.focus();
MemberReturnContract
handleKeydown(event)ListKeyResult<T> | nullReturns explicit handled/change state for navigation and typeahead keys.
navigate(action)ListNavigationChange<T> | nullMoves programmatically and returns the committed change.
set(index)numberSets an integer usable index, or resets to -1.
reset()voidClears the active index and typeahead sequence.
getIndex()numberReturns the current usable index, or -1.
getActiveItem()T | undefinedReturns the item at the current usable index.

Boundary navigation keys return { handled: true, change: null } and remain consumed. Unrecognized, already-prevented, composing, and disabled events return null. Successful typeahead returns a change and follows typeahead.preventDefault.

Keys are matched through matchKey from @vielzeug/keymap, so keys overrides accept shortcut patterns with aliases (esc, space, up) and modifiers (shift+Home), and modifier state must match exactly: a plain ArrowDown binding does not fire on Ctrl+ArrowDown.


createGridNavigation() ​

ts
function createGridNavigation<T>(options: GridNavigationOptions<T>): GridNavigation<T>;

Creates a two-dimensional keyboard navigation controller: horizontal arrows step one item, vertical arrows step one row (the column count), Home/End jump to the grid's ends.

ParameterTypeDescription
optionsGridNavigationOptions<T>Item lookup, column resolution, active-index source, key mapping, and wrapping options.

Returns: GridNavigation<T>.

Example

ts
import { createGridNavigation } from '@vielzeug/focus';

const grid = createGridNavigation<HTMLElement>({
  columns: 4,
  getActiveIndex: () => tiles().indexOf(document.activeElement as HTMLElement),
  getItems: tiles,
});

const result = grid.handleKeydown(event);
if (result?.change) result.change.item.focus();
MemberReturnContract
handleKeydown(event)GridKeyResult<T> | nullReturns handled state for navigation keys; recognized keys are always consumed, even at clamped edges.
navigate(action)GridNavigationChange<T> | nullMoves programmatically and returns the committed change.
set(index)voidSets the tracked index for grids without getActiveIndex.
reset()voidClears the tracked index.
getIndex()numberReturns the active index: derived from getActiveIndex when provided, or -1.
getActiveItem()T | undefinedReturns the item at the active index.

Unlike createListNavigation, items are not skipped when disabled: skipping in two dimensions would break row alignment. With no active index, forward moves start at the first item and backward moves at the last. columns may be a getter resolved on every navigation, so responsive grids can read a media query and measured grids can read the rendered row length. FocusConfigError is thrown when columns resolves below 1.


createQuickLookGrid() ​

ts
function createQuickLookGrid(options: QuickLookGridOptions): QuickLookGrid;

Creates keyboard browsing over a tile grid with a Quick Look key: arrows and Home/End move focus through the items (delegating to createGridNavigation()) and inspectKey previews the item under the event instead of activating it: the pair every card picker needs.

ParameterTypeDescription
optionsQuickLookGridOptionsItem lookup, active-index source, column resolution, preview key, wrapping, and inspection callback.

Returns: QuickLookGrid.

Example

ts
import { createQuickLookGrid } from '@vielzeug/focus';

const quickLook = createQuickLookGrid({
  getItems: () => [...grid.querySelectorAll<HTMLElement>('.tile')],
  onInspect: (item, index) => previewCard(index),
});

grid.addEventListener('keydown', (event) => quickLook.handleKeydown(event));
MemberReturnContract
handleKeydown(event)voidRoutes one keydown: navigation keys move focus to the reported item, inspectKey calls preventDefault()/stopPropagation() then onInspect(item, index). Unrecognized, already-prevented, composing, modifier, and disabled events pass through.

columns defaults to the rendered row length at the active item (items sharing its top edge), which follows the grid's own responsive wrapping without a media query; supply columns to read a track list instead. getActiveIndex defaults to the item that is or contains document.activeElement; supply it when focus sits on a wrapper around the item. inspectKey defaults to ' ' (Space), the platform preview key. The recognizer pairs with a pointer long-press at the call site as the touch counterpart of onInspect.


restoreFocus() ​

ts
function restoreFocus(target: FocusTarget, options?: RestoreFocusOptions): boolean;

Attempts to focus a connected target that is neither disabled nor inert. Throwing target getters or focus() implementations count as failed attempts and fall through to the configured fallback.

ParameterTypeDescription
targetFocusTargetElement or getter resolved when restoreFocus() is called.
optionsRestoreFocusOptionsOptional lazy fallback and preventScroll flag.

Returns: boolean: true when focus moved to the target or fallback.

Example

ts
import { restoreFocus } from '@vielzeug/focus';

restoreFocus(() => triggerElement, {
  fallback: () => document.body,
  preventScroll: true,
});

rescueFocus() ​

ts
function rescueFocus(target: FocusTarget, options?: RestoreFocusOptions): boolean;

Hands focus to target when focus has been lost to the document body: the state left behind when the focused element unmounts mid-swap, where keydown never reaches a handler.

ParameterTypeDescription
targetFocusTargetElement or getter resolved when the rescue runs.
optionsRestoreFocusOptionsOptional lazy fallback and preventScroll flag, as in restoreFocus().

Returns: boolean: true when focus was rescued; false when focus is already on a real element or neither target can receive focus.

Example

ts
import { rescueFocus } from '@vielzeug/focus';

// After a swap that may have unmounted the focused element.
rescueFocus(() => dialog.querySelector<HTMLElement>('footer button'));

captureFocus() ​

ts
function captureFocus(options?: CaptureFocusOptions): FocusRestorer;

Captures the deepest active element immediately and returns a one-shot restoration function.

ParameterTypeDescription
optionsCaptureFocusOptionsOptional lazy fallback, preventScroll, and cancellation signal.

Returns: FocusRestorer. Its first call attempts restoration; later calls return false.

Example

ts
import { captureFocus } from '@vielzeug/focus';

const restore = captureFocus({ fallback: () => document.body });

dialog.showModal();
dialog.addEventListener('close', restore, { once: true });

Types ​

ts
type MaybeGetter<T> = T | (() => T);

type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';
type ListKeyAction = ListNavigationAction | 'typeahead';

type ListNavigationChange<T> = {
  readonly action: ListKeyAction;
  readonly event?: KeyboardEvent;
  readonly index: number;
  readonly item: T;
};

type ListKeyResult<T> = {
  readonly change: ListNavigationChange<T> | null;
  readonly handled: true;
};

type ListNavigationTypeaheadOptions<T> = {
  delayMs?: number;
  getLabel: (item: T, index: number) => string;
  preventDefault?: boolean;
};

type ListNavigationOptions<T> = {
  direction?: MaybeGetter<'ltr' | 'rtl'>;
  disabled?: MaybeGetter<boolean>;
  getItems: () => readonly T[];
  isItemDisabled?: (item: T, index: number) => boolean;
  keys?: Partial<Record<ListNavigationAction, readonly string[]>>; // shortcut patterns matched via @vielzeug/keymap
  loop?: boolean;
  orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;
  typeahead?: ListNavigationTypeaheadOptions<T>;
};

type ListNavigation<T> = {
  getActiveItem(): T | undefined;
  getIndex(): number;
  handleKeydown(event: KeyboardEvent): ListKeyResult<T> | null;
  navigate(action: ListNavigationAction): ListNavigationChange<T> | null;
  reset(): void;
  set(index: number): number;
};

type GridNavigationAction = 'first' | 'last' | 'next' | 'nextRow' | 'prev' | 'prevRow';

type GridNavigationChange<T> = {
  readonly action: GridNavigationAction;
  readonly event?: KeyboardEvent;
  readonly index: number;
  readonly item: T;
};

type GridKeyResult<T> = {
  readonly change: GridNavigationChange<T> | null;
  readonly handled: true;
};

type GridColumns = number | (() => number);

type GridNavigationOptions<T> = {
  columns: GridColumns;
  direction?: MaybeGetter<'ltr' | 'rtl'>;
  disabled?: MaybeGetter<boolean>;
  getActiveIndex?: () => number;
  getItems: () => readonly T[];
  keys?: Partial<Record<GridNavigationAction, readonly string[]>>;
  loop?: boolean;
};

type GridNavigation<T> = {
  getActiveItem(): T | undefined;
  getIndex(): number;
  handleKeydown(event: KeyboardEvent): GridKeyResult<T> | null;
  navigate(action: GridNavigationAction): GridNavigationChange<T> | null;
  reset(): void;
  set(index: number): void;
};

type QuickLookGridOptions = {
  columns?: GridColumns;
  disabled?: MaybeGetter<boolean>;
  getActiveIndex?: () => number;
  getItems: () => readonly HTMLElement[];
  inspectKey?: string;
  loop?: boolean;
  onInspect: (item: HTMLElement, index: number) => void;
};

type QuickLookGrid = {
  handleKeydown(event: KeyboardEvent): void;
};

type FocusTarget = HTMLElement | SVGElement | null | undefined | (() => HTMLElement | SVGElement | null | undefined);

type RestoreFocusOptions = {
  fallback?: FocusTarget;
  preventScroll?: boolean;
};

type CaptureFocusOptions = RestoreFocusOptions & {
  signal?: AbortSignal;
};

type FocusRestorer = () => boolean;

typeahead.delayMs defaults to 500; supplied values must be positive and finite. preventDefault defaults to false, which preserves editable combobox input. Set it to true for menu-style typeahead.

Errors ​

ErrorTriggerNotable properties
FocusErrorBase class for every focus-originated errorinstanceof FocusError catches any focus error
FocusConfigErrorInvalid navigation configurationExtends FocusError. Thrown for a key assigned to two actions, a non-positive typeahead.delayMs, or grid columns resolving below 1