Skip to content

Listbox

hooklistboxnavigationselectionkeyboardaria

Shared listbox state and keyboard navigation hook. Manages selection, highlight, and container ARIA props for listbox-pattern components like menu and navigation-list.

tsx
const items = [  { id: "apple", label: "Apple" },  { id: "banana", label: "Banana" },];const { selectedId, highlighted, handleItemActivate, getContainerProps } =  useListbox({    idPrefix: "my-list",    items: items.map((item) => ({ id: item.id })),    onSelect: (id) => console.log("selected", id),  });return (  <div {...getContainerProps()} aria-label="Fruit choices">    {items.map((item) => (      <div        key={item.id}        id={`my-list-${item.id}`}        role="option"        data-value={item.id}        aria-selected={selectedId === item.id}        onClick={() => handleItemActivate(item.id)}      >        {item.label}      </div>    ))}  </div>);

Installation

$pnpm exec dgadd add ui/listbox
[Installs to]src/hooks/use-listbox.ts[Item]ui/listbox

dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.

UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.

Parameters

NameTypeDefaultDescription
idPrefixrequiredstringPrefix for generating aria-activedescendant IDs. By default, each option uses id="${idPrefix}-${encodeURIComponent(itemId)}" via getEncodedListboxItemId; pass getItemId to use a different encoding.
autoFocusbooleanfalseFocus the container on mount and initialize highlight to the selected item or first navigable item for the active role.
selectedIdstring | nullControlled selected item ID. When provided, the hook is in controlled mode for selection.
defaultSelectedIdstring | nullnullInitial selected item ID for uncontrolled mode.
highlightedstring | nullControlled highlighted item ID. When provided, the hook is in controlled mode for highlight.
defaultHighlightedstring | nullnullInitial highlighted item ID for uncontrolled mode.
onSelect(id: string) => voidCalled when an item is activated by click, Space, or Enter, including re-activating the already-selected item.
onEnter(id: string, event: KeyboardEvent) => voidCalled when Enter activates the highlighted item. Selection is committed before this callback runs.
onHighlightChange(id: string | null) => voidCalled when the highlighted item changes via keyboard navigation or is cleared.
onNavigationBoundaryReached(direction: "previous" | "next", event: KeyboardEvent, key: string) => voidCalled when wrap is false and keyboard navigation attempts to move before the first item or after the last item. Receives the direction, the originating keyboard event, and the key that hit the boundary.
wrapbooleantrueWhether keyboard navigation wraps from last item to first (and vice versa).
onKeyDown(event: KeyboardEvent) => voidAdditional keydown handler called before the built-in navigation handler. Call event.preventDefault() to suppress default keyboard navigation.
role"listbox" | "menu""listbox"ARIA role for the container element.
itemRole"option" | "menuitem" | "menuitemradio""option"ARIA role for each item element.
typeaheadbooleanfalseEnable type-ahead character search to jump to matching items.
itemsListboxMetadataItem[]Optional metadata array describing each item ({ id, disabled? }). It helps validate active descendants and initial highlight, while keyboard navigation and typeahead still inspect the mounted DOM items.
getItemId(idPrefix: string, id: string) => stringgetEncodedListboxItemIdOverride how option DOM ids are derived from idPrefix and the item's logical id. Defaults to URL-encoding the id; supply a custom encoder if your option ids must follow a different scheme (e.g. when consuming externally indexed nodes).
refRef<HTMLDivElement>External/forwarded ref for the container. Composed once with the internal ref so getContainerProps returns a stable ref callback (pass a stable ref, e.g. from useComposedRefs, to avoid detach/re-attach).

Returns

UseListboxReturnObject with selection state, highlight state, event handlers, and a prop-getter for the container element.
NameTypeDefaultDescription
selectedIdrequiredstring | nullCurrently selected item ID.
highlightedrequiredstring | nullCurrently highlighted (focused) item ID.
handleItemActivaterequired(id: string) => voidCall on item click or Enter/Space to select and activate it.
handleItemHighlightrequired(id: string | null) => voidCall on item hover/focus to highlight it. Pass null to clear the highlight.
getContainerPropsrequired() => ContainerPropsProp-getter for the listbox container. Returns ref, role, tabIndex, aria-activedescendant, and onKeyDown. The ref composes the internal ref with the `ref` option passed to useListbox.

Examples

Basic Listbox

Preview

Notes

Keyboard Navigation

Arrow keys and their vim aliases j/k move highlight through items with role="option" inside the container. Enter and Space select the highlighted item. With typeahead enabled, j/k still move the highlight on an empty query buffer and only extend a query already in progress. Navigation uses @diffgazer/keys's useNavigation internally.

Controlled & Uncontrolled

Both selection and highlight support controlled and uncontrolled modes via useControllableState. Pass selectedId/highlighted for controlled, or use defaultSelectedId/defaultHighlighted for uncontrolled.

Used By

Built into menu and navigation-list components. Provides the shared listbox interaction pattern so each component only needs to handle its own rendering.

Source

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