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

Listbox#

The mid-level part set for command palettes and custom listboxes: a text input driving a static option list, wired as the APG combobox pattern with aria-activedescendant. You own the option set (querying, filtering, ordering); the parts own ids, ARIA wiring, keyboard navigation, the highlight, commit, and screen-reader announcements. It ships unstyled: style it through the data-highlighted, data-selected, data-disabled, and data-empty contract.

Use Listbox when you are building the list yourself, a command palette, a search dialog, a custom picker, and only need the interaction layer. Select and Combobox remain the full pickers: they own the value, float the popup, and filter for you. Listbox holds no value and does not float; the list renders where you put it.

Demo#

Focus the input and use the arrow keys: the highlight moves while focus stays on the input, and Enter commits the highlighted option. The first result is highlighted whenever the set changes, so Enter always runs the top match.

No results
Open file…
Go to symbol…
Toggle theme
Restart server
Copy deep link

Pick a command to run it.

Command palette#

A palette is the same structure inside a Dialog: the dialog owns open state, focus, and Escape; pass its open state to enabled so navigation and announcements sleep while closed. This is exactly how this site's own ⌘K palette is built.

<Dialog.Root open={open} onOpenChange={setOpen}>
  <Dialog.Popup aria-label="Command palette">
    <Listbox.Root enabled={open} onCommit={run}>
      <Listbox.Input
        aria-label="Search commands"
        value={query}
        onInput={(e) => setQuery(e.currentTarget.value)}
      />
      <Listbox.Status />
      <Listbox.List aria-label="Commands">
        {results.map((r) => (
          <Listbox.Option key={r.id} value={r.id}>
            {r.title}
          </Listbox.Option>
        ))}
        <Listbox.Empty>No results</Listbox.Empty>
      </Listbox.List>
    </Listbox.Root>
  </Dialog.Popup>
</Dialog.Root>

enabled={open} also keeps the opening announcement honest: Listbox.Status renders an empty string while disabled, so opening the palette is a real text change ('' -> '5 results available') that screen readers read out.

Keyboard#

KeyContextAction
ArrowDown / ArrowUpInput focusedMove the highlight (wraps by default); focus stays on the input.
EnterAn option highlightedCommit the highlighted option (onCommit).
EnterNo optionsNot consumed; native form behavior proceeds.
Printable charactersInput focusedNever consumed: typing always reaches the input.
Home / EndInput focusedNot consumed: they move the caret, not the list.

Typeahead is off by design: the input is the text field, so printable keys must type, not jump the list.

Accessibility#

The parts implement the APG combobox pattern with a listbox popup, using aria-activedescendant so DOM focus never leaves the input:

  • Listbox.Input is role="combobox" with aria-expanded, aria-controls pointing at the list, and aria-activedescendant tracking the highlight while enabled.
  • Listbox.List is role="listbox"; each Listbox.Option is role="option" with aria-selected and aria-disabled.
  • Listbox.Status is a polite, atomic live region announcing the result count after each change.

When the listbox has no real selection (a command palette), aria-selected follows the highlight, which is the convention palettes use so screen readers announce the pointed-at result. When you do track a selection, pass selected explicitly and the highlight stays visual-only (data-highlighted).

API reference#

Every part accepts a render prop for composition (see renderElement) and forwards unknown props to the element it renders. Listbox.Option passes { selected, disabled, highlighted } to a render function; Listbox.Input passes { enabled }; Listbox.Status passes { count, enabled }.

Listbox.Root#

Provides the highlight state, ids, refs, option registry, and the commit callback to the parts. Generic over the option value type (Listbox.Root<Value>); defaults to string. Renders no element of its own.

PropTypeDefaultDescription
onCommit(value: Value) => void-Called when an option is committed (Enter on the highlight, or a click).
enabledbooleantrueKeyboard navigation, auto-highlight, and announcements are live while true. Pass a dialog's open state for a palette.
loopbooleantrueWrap the highlight from last to first and back.
childrenComponentChildren-The other parts.

The Root holds no selected value. Consumers that need one keep it themselves and mark options with selected.

Listbox.Input#

The text field driving the list. Owns keyboard navigation while enabled; focus stays on it and aria-activedescendant tracks the highlight. Default element <input type="text"> with role="combobox".

PropTypeDefaultDescription
renderRenderProp<{ enabled: boolean }>-Compose or replace the element.
...propsJSX.HTMLAttributes<HTMLInputElement>-Forwarded to the element; a passed onKeyDown runs before navigation and can preventDefault to suppress it.

Sets role="combobox", autocomplete="off", aria-expanded, aria-controls, and aria-activedescendant (while enabled).

Listbox.List#

The option container, rendered in place (it does not float). Default element <div> with role="listbox".

PropTypeDefaultDescription
renderRenderProp-Compose or replace the element.
aria-labelstring-Accessible name for the list.
childrenComponentChildren-The options (and an optional Empty).
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Sets role="listbox", the id Listbox.Input points at, and data-empty while no options are registered.

Listbox.Option#

One committable row. Default element <div> with role="option". Generic over the root's value type (Listbox.Option<Value>).

PropTypeDefaultDescription
valueValue-Passed to onCommit when this option is committed.
selectedboolean-aria-selected. Defaults to the highlight when omitted (the command-palette convention).
disabledbooleanfalseSkipped by navigation; does not commit on click.
renderRenderProp<{ selected, disabled, highlighted: boolean }>-Compose or replace the element.
childrenComponentChildren-The row content.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element; passed onClick / onPointerEnter run before commit / highlight.

Sets role="option", aria-selected, aria-disabled (when disabled), and data-highlighted / data-disabled while each state holds. data-selected appears only when you pass selected explicitly: the highlight already has its own attribute, so selection styles (a checkmark, say) never fire on a merely-highlighted row even though aria-selected defaults to the highlight. Hovering highlights; clicking commits.

Listbox.Status#

A visually-hidden polite live region announcing the result count: 'No results', or 'N results available'. Empty while disabled, so enabling (a palette opening) is a real text change that screen readers announce. Default element <div>.

PropTypeDefaultDescription
renderRenderProp<{ count: number; enabled: boolean }>-Replace the message (e.g. to announce loading) while keeping the region.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded (excluding children); merged with the visually-hidden style.

Sets role="status", aria-live="polite", aria-atomic="true", and visually-hidden styles.

Listbox.Empty#

Rendered only while enabled with zero options registered; put the "no results" row in it. Default element <div> with role="presentation" (the live announcement comes from Listbox.Status, not this element).

PropTypeDefaultDescription
renderRenderProp-Compose or replace the element.
childrenComponentChildren-The empty-state content.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.