hono-preact
Overview
Quick Start
The Route Table
Layouts & Nesting
Adding Pages
Active Links
Server Loaders
Loading States
Reloading Data
Prefetching
Streaming
Live Loaders
Realtime Channels
Server Actions
Validation
Optimistic UI
View Transitions
Middleware
CSRF Protection
CLI
Vite Config
Project Structure
Composing Hono Middleware
WebSockets
Rooms & Presence
renderPage
Link Prefetch
Build & Deploy
Overview
Dialog
Popover
Tooltip
Menu
Context Menu
Select
Combobox
Toast
renderElement
useControllableState
mergeRefs
useListNavigation
useTypeahead
useListboxSelection
usePosition
usePositioner
useDismiss
useFocusReturn
useSafeArea
usePresence

useListNavigation#

useListNavigation is the keyboard navigation binding the Menu and Select components use. Given a container of items, it handles ArrowUp / ArrowDown, Home / End, and typeahead, tracking which item is active and moving the active state through the list (wrapping at the ends, skipping disabled items).

It supports two modes. In roving mode it moves DOM focus to the active item (a roving tabindex list, as in a menu). In activedescendant mode focus stays on a container element and you render aria-activedescendant pointing at the active item, scrolling it into view rather than focusing it (as in a listbox where the trigger keeps focus).

Demo#

Example#

A listbox where the trigger keeps focus and the active option is tracked with aria-activedescendant:

import { useListNavigation } from 'hono-preact-ui';
import { useRef, useState } from 'preact/hooks';

function Listbox({ open }: { open: boolean }) {
  const listRef = useRef<HTMLDivElement>(null);
  const [activeId, setActiveId] = useState<string | null>(null);
  const nav = useListNavigation({
    enabled: open,
    containerRef: listRef,
    itemSelector: '[role="option"]:not([aria-disabled="true"])',
    activeId,
    setActiveId,
    mode: 'activedescendant',
  });
  return (
    <button
      role="combobox"
      aria-expanded={open}
      aria-activedescendant={open ? (activeId ?? undefined) : undefined}
      onKeyDown={nav.onKeyDown}
    >
      <div ref={listRef} role="listbox">
        {/* role="option" rows */}
      </div>
    </button>
  );
}

Signature#

import { useListNavigation } from 'hono-preact-ui';

function useListNavigation(opts: UseListNavigationOptions): ListNavigation;

interface UseListNavigationOptions {
  enabled: boolean;
  containerRef: RefObject<HTMLElement>;
  itemSelector: string;
  activeId: string | null;
  setActiveId: (id: string | null) => void;
  mode: 'roving' | 'activedescendant';
  loop?: boolean; // default true
  typeahead?: boolean; // default true
  homeEnd?: boolean; // default true
  scopeSelector?: string;
}

interface ListNavigation {
  onKeyDown: (event: KeyboardEvent) => void;
  getItems: () => HTMLElement[];
  setActiveItem: (index: number) => void;
}

Options#

OptionTypeDefaultNotes
enabledbooleannoneHandle keys only while active (typically the open state).
containerRefRefObject<HTMLElement>noneThe element holding the items.
itemSelectorstringnoneCSS selector matching the navigable items (exclude disabled items here).
activeIdstring | nullnoneThe id of the active item; drives aria-activedescendant.
setActiveId(id: string | null) => voidnoneCalled to change the active item.
mode'roving' | 'activedescendant'noneroving moves DOM focus; activedescendant keeps focus and scrolls into view.
loopbooleantrueWrap arrow navigation at the first / last item.
typeaheadbooleantrueActivate items by typing their leading characters.
homeEndbooleantrueHandle Home/End (default true); pass false to leave them as native caret movement (Combobox uses this).
scopeSelectorstringnoneExclude items nested in a closer same-role container (a submenu).

Returns#

FieldTypeDescription
onKeyDown(event: KeyboardEvent) => voidHandle navigation keys; calls preventDefault on keys it consumes.
getItems() => HTMLElement[]The current enabled items, queried live from the DOM in order.
setActiveItem(index: number) => voidActivate the item at index: sets the active id and focuses or scrolls it per mode.

onKeyDown calls event.preventDefault() on the keys it handles, so a caller that adds its own keys (Enter to select, Escape to close) can early-return on event.defaultPrevented after delegating.

Companion exports#

hono-preact-ui also exports useTypeahead, the type-to-select hook useListNavigation uses internally, for composing your own navigation:

ExportTypeDescription
useTypeahead(opts?: UseTypeaheadOptions) => (char: string) => stringA hook returning a callback that accumulates printable chars into a query, resetting after an idle gap.