Skip to content

Navigation

DOM-driven navigation hooks for lists, menus, and tab bars with arrow keys, Home/End, and Enter/Space.

DOM-driven navigation hooks for lists, menus, and tab bars. Query elements by [role] and data-value attributes, handle arrow keys, Home/End, Enter/Space, and scroll-into-view.

Info:

For the complete API reference, see the @diffgazer/keys documentation.


useNavigation

Standalone hook — no KeyboardProvider required. Returns an onKeyDown handler for manual attachment.

tsx
import { useNavigation } from "@diffgazer/keys"
// or in copy mode:
// import { useNavigation } from "@/hooks/use-navigation"

function FileList() {
  const containerRef = useRef<HTMLDivElement>(null)
  const getOptionId = (value: string) => `file-${value}`

  const { highlighted, isHighlighted, onKeyDown } = useNavigation({
    containerRef,
    role: "option",
    onSelect: (value) => toggleItem(value),
    onEnter: (value) => openItem(value),
    wrap: true,
  })

  return (
    <div
      ref={containerRef}
      role="listbox"
      aria-activedescendant={highlighted ? getOptionId(highlighted) : undefined}
      tabIndex={0}
      onKeyDown={onKeyDown}
    >
      <div
        id={getOptionId("readme")}
        role="option"
        data-value="readme"
        data-highlighted={isHighlighted("readme") ? "" : undefined}
      >
        README.md
      </div>
      <div
        id={getOptionId("package")}
        role="option"
        data-value="package"
        data-highlighted={isHighlighted("package") ? "" : undefined}
      >
        package.json
      </div>
    </div>
  )
}

Key options

OptionDefaultDescription
containerRefRef to the scrollable container
role"radio" | "checkbox" | "option" | "menuitem" | "menuitemcheckbox" | "menuitemradio" | "button" | "tab"
wraptrueWrap around at boundaries
orientation"vertical""vertical" or "horizontal" (determines arrow key direction)
onSelectSpace key callback
onEnterEnter key callback (falls back to onSelect)
onHighlightChangeCalled when keyboard navigation changes highlight
onNavigationBoundaryReachedCalled with direction, native event, and key when wrap={false} hits an edge
moveFocusfalseMove DOM focus to the highlighted item instead of only updating highlight state
scopeToContainertrueExclude nested composite widgets from the item query

DOM requirements

  • Items must have [role="${role}"] and data-value attributes, or the data-diffgazer-navigation-item contract from getNavigationItemProps()
  • Calls scrollIntoView({ block: "nearest" }) on focus changes
  • Skips [aria-disabled="true"] elements by default
  • Typed data-diffgazer-navigation-item values keep mixed widgets separate, for example option navigation does not pick up radio items in the same subtree

With @diffgazer/ui components

All navigation-enabled @diffgazer/ui components (Menu, Tabs, Select, RadioGroup, etc.) use useNavigation internally. See Patterns for integration examples.