Skip to content

useScopedNavigation

provider-dependentnavigationscope

Scope-aware keyboard navigation registered via KeyboardProvider. Use when navigation should respect the scope stack (e.g., modals, panels).

useScopedNavigation is the provider-backed sibling of useNavigation. It shares the same DOM item-discovery contract and the same options, but registers its keys through KeyboardProvider instead of returning an onKeyDown handler. Because dispatch goes through the provider, navigation participates in the scope stack and can listen at the document level without the list being focused first.

tsx
const { highlighted } = useScopedNavigation({  containerRef,  role: "menuitem",})

When to use it

  • The list is the primary content of a panel, dialog, drawer, or command palette and arrow keys should work immediately.
  • Navigation must be suppressed while a deeper scope (a nested overlay) is active.
  • You want page-level keys via focusWithinOnly rather than wiring onKeyDown onto a focusable container.

For a list embedded inside a self-contained component — a combobox popup, a dropdown — prefer useNavigation; it avoids the provider dependency.

Typing data-value (TValue)

useScopedNavigation re-exposes the same TValue generic as useNavigation. It narrows highlighted, onHighlightChange, onSelect, and onEnter to a literal union for editor convenience, but it is a type assertion, not runtime validation: every value originates from the data-value DOM attribute and is asserted to TValue at the DOM boundary without being checked against the union. Keep your data-value attributes in sync with the union you instantiate, or validate the string inside your callback before relying on it.

Installation

Requires KeyboardProvider and the @diffgazer/keys package, which is not public on npm yet.

Diffgazer packages are not yet published to npm. Until the first release, pack @diffgazer/ui and @diffgazer/keys from the repository and install those tarballs.

@diffgazer/keys requires no CSS or Tailwind setup.

Parameters

NameTypeDefaultDescription
containerRefrequiredRefObject<HTMLElement | null>Ref to the container element holding navigable items.
rolerequired"radio" | "checkbox" | "option" | "menuitem" | "menuitemcheckbox" | "menuitemradio" | "button" | "tab"ARIA role used to query navigable children within the container.
highlightedstring | nullControlled highlight value.
onHighlightChange(value: string | null) => voidCalled when the controlled highlight value should change. Receives null when highlight is cleared.
onSelect(value: string, event: KeyboardEvent) => voidCalled when Space selects the highlighted item, and as the Enter fallback when onEnter is not provided.
onEnter(value: string, event: KeyboardEvent) => voidCalled when Enter is pressed on the highlighted item. When provided, it overrides the onSelect Enter fallback.
wrapbooleantrueWrap around when reaching the first or last item.
enabledbooleantrueWhether the navigation hook is active.
preventDefaultbooleantrueCall preventDefault() on handled keyboard events.
onNavigationBoundaryReached(direction: "previous" | "next", event: KeyboardEvent, key: string) => voidCalled when the user tries to navigate past the first or last item. Receives the orientation-neutral direction, the originating keyboard event, and the key that hit the boundary.
defaultHighlightedstring | nullnullInitial highlighted value in uncontrolled mode.
upKeysstring[]Custom key names to move highlight up/left.
downKeysstring[]Custom key names to move highlight down/right.
orientation"vertical" | "horizontal""vertical"Navigation axis.
skipDisabledbooleantrueSkip items with aria-disabled="true", data-disabled, or native disabled during navigation.
moveFocusbooleanfalseMove DOM focus to the next item instead of only updating highlight state.
scopeToContainerbooleantrueIgnore items owned by nested collection containers such as nested radiogroups, listboxes, menus, or tablists.
ownerSelectorstring | nullAdvanced owner selector override for scoping roles that do not have a standard composite owner.
scopestring | nullKeyboard scope name to register navigation handlers under. Pass null to skip registration while a conditional scope is disabled.
focusWithinOnlybooleanfalseOnly handle navigation keys when focus is within the container element.
allowInInputbooleanfalseAllow navigation keys while a text-editable element has focus.

Returns

UseScopedNavigationReturnObject with highlight state. No onKeyDown — keys are registered via the provider.
NameTypeDefaultDescription
highlightedrequiredstring | nullThe value of the currently highlighted item, or null.
isHighlightedrequired(value: string) => booleanReturns true if the given value is the highlighted item.
highlightrequired(value: string | null) => voidImperatively set the highlighted item. Pass null to clear.

Unlike useNavigation, there is no returned onKeyDown — the provider owns key dispatch.

Keyboard behavior

KeyBehavior
ArrowDown / ArrowUpMove highlight (vertical orientation).
ArrowRight / ArrowLeftMove highlight (horizontal orientation).
Home / EndJump to the first / last item.
EnterCalls onEnter(value), falling back to onSelect(value).
SpaceCalls onSelect(value).

Editable-target filtering is handled by the provider via allowInInput. Scope resolution means that if a deeper scope is active, this hook's bindings are suppressed until it is popped.

Examples

Scoped navigation in a menu

Preview

Two lists with focusWithinOnly

Preview

Notes

Requires KeyboardProvider

useScopedNavigation registers keys through the KeyboardProvider context. It must be used within a <KeyboardProvider> tree.

Scope-aware

Navigation bindings respect the scope stack. If a deeper scope is active, this hook's bindings are suppressed.

No onKeyDown needed

Unlike useNavigation, you do not need to wire up an onKeyDown handler. The provider handles key dispatch.

Edge cases

  • Missing provider. This hook asserts the provider context on mount and throws a clear error if no KeyboardProvider is present — unlike useKey, it does not silently no-op.
  • scope: null. Pass null to skip registration while a conditional scope is disabled, without removing the Hook call.
  • focusWithinOnly. Set it when several navigable regions share one page-level provider so each region only responds while it holds focus.
  • useNavigation — the standalone version with an onKeyDown handler.
  • useScope — push the scope this hook registers under.
  • Scopes guide — how the scope stack resolves competing bindings.

Source

Highlighted source loads after this disclosure opens. Browse the source repository.