useNavigation
Standalone keyboard navigation for role-based lists. Uses DOM queries to find navigable items. No provider needed.
useNavigation adds arrow-key navigation, selection, and focus tracking to any list of role-attributed elements. It is standalone — it returns an onKeyDown handler you attach to the container and needs no provider. Items are discovered from the DOM by ARIA role, so the hook stays declarative and works with whatever markup you already render.
When to use it
- Listboxes, menus, radio groups, tab bars, and command lists embedded inside a single component.
- Cases where you control the container and can attach
onKeyDowndirectly. - Copy-paste consumers that want list navigation without pulling in
KeyboardProvider.
When arrow keys should work without the user first clicking into the list, or the list is the main content of a panel that must respect a scope stack, use useScopedNavigation instead. For deeper guidance see the navigation guide.
DOM contract
Three things must hold for item discovery to work:
- A container element with a ref passed as
containerRef. - Child elements with a matching
roleattribute. - Each child carries a
data-valueattribute — the value the hook reports ashighlightedand passes toonSelect/onEnter.
Typing data-value (TValue)
The TValue generic 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; the hook never checks that the attribute actually belongs to your union. Instantiating with useNavigation<"a" | "b"> makes TypeScript treat the values as "a" | "b", yet a stray data-value="c" is still delivered with full type confidence. Keep your data-value attributes in sync with the union you instantiate, or validate the string inside your callback before relying on it.
Installation
dgadd ships in @diffgazer/add on npm: pnpm add -D @diffgazer/add puts it on pnpm exec, and npx @diffgazer/add runs it without the install.
@diffgazer/keys requires no CSS or Tailwind setup.
Parameters
Returns
Keyboard behavior
Selection mirrors ARIA semantics: Space toggles, Enter activates. On every move the hook calls scrollIntoView({ block: "nearest" }) so long lists keep the focused item visible. Custom keys can be supplied with upKeys / downKeys (for example vim-style j / k).
Examples
Basic list navigation
Horizontal tab navigation
Notes
Standalone
useNavigation does not require KeyboardProvider. It works with a direct onKeyDown handler attached to the container.
DOM-based item discovery
Navigable items are queried from the DOM using the specified role or the data-diffgazer-navigation-item data contract. Items must have a data-value attribute.
Nested collections
scopeToContainer is enabled by default so items inside a nested owner container are excluded from the parent navigation order.
Controlled and uncontrolled
Pass highlighted + onHighlightChange for controlled mode, or use defaultHighlighted for uncontrolled mode.
Edge cases
- Disabled items. With
skipDisabled(default), items exposingaria-disabled="true",data-disabled, or nativedisabledare skipped during movement. - Nested collections.
scopeToContainer(default) excludes items owned by a nested collection of the same family — an outer radiogroup ignores radios inside an inner radiogroup. - Boundaries. With
wrap: false, hitting the first or last item firesonNavigationBoundaryReached(direction, event, key)instead of wrapping — useful for moving focus to an adjacent region. moveFocus. By default the hook only updates highlight state. SetmoveFocusto also move DOM focus to the highlighted element (common forrole="tab").
Related
useScopedNavigation— the provider-backed, scope-aware counterpart.- Navigation guide — controlled vs uncontrolled, orientation, and tab patterns.
- Utilities — the DOM helpers behind item discovery.
Source
Highlighted source loads after this disclosure opens. Browse the source repository.