Command Menu

A searchable command palette built on Base UI Autocomplete, with grouped, virtualized rows and controlled search state.

Installation

import { CommandMenu } from "@o/pipeline/experimental/components/command-menu";

Usage

CommandMenu is a compound set of parts backed by Base UI Autocomplete. You own the search input value, controlled via value / onValueChange or uncontrolled via defaultValue. Pass groups in as data (virtualization needs them for sizing), and render each row through the Items render function.

  • The item type is yours: items extend CommandMenuItem (id / searchValue / keywords) with whatever a row renders (label, symbol, onClick, href, …). Annotate the Items render param with that type, since like Base UI’s Autocomplete, it isn’t inferred from Root’s items.
  • Filtering: applied to the static items internally. Rows match on searchValue, the group label, and any cmdk-style keywords (e.g. a company, domain, or email): related info that isn’t part of the visible label.
  • Selection: Base UI’s onClick on Item; fires on pointer click and on Enter when the row is highlighted.
  • Warm on intent: Root’s onItemHighlighted (Base UI) fires on keyboard arrow and pointer hover, useful for prefetching or preloading a row’s destination before selection.
<CommandMenu.Root value={query} onValueChange={setQuery} items={groups}>
  <CommandMenu.Input />
  <CommandMenu.List>
    {loading && <CommandMenu.Loading />}
    {!loading && <CommandMenu.Empty />}
    <CommandMenu.Items>
      {(item: CommandItem) => (
        <CommandMenu.Item
          key={item.id}
          value={item}
          symbol={item.symbol}
          kbd={item.kbd}
          onClick={item.onClick}
        >
          {item.label}
        </CommandMenu.Item>
      )}
    </CommandMenu.Items>
  </CommandMenu.List>
  <CommandMenu.Footer />
</CommandMenu.Root>

Composing with Dialog

Warning

CommandMenu is designed to be used within a Dialog component, and may not work as expected in other contexts.

Async results and status rows

Remote and async results are your responsibility: compute items from the controlled value, and drive the status rows from your own fetch state.

CommandMenu.Empty and CommandMenu.Loading are status rows (wrapping Base UI’s Empty / Status) composed inside List. The three states (items, no results, and searching) are mutually exclusive: both rows hide themselves once there are matches, so they only ever show in place of an empty list. Render Loading while a request is in flight and Empty otherwise:

<CommandMenu.List>
  {isFetching && <CommandMenu.Loading />}
  {!isFetching && <CommandMenu.Empty />}
  <CommandMenu.Items>{(item) => /* … */}</CommandMenu.Items>
</CommandMenu.List>

Empty and Loading share the same shape (an icon prop and caption children, each with a default), so you customize either one the same way:

<CommandMenu.Empty icon={<IconInbox />}>Nothing here yet</CommandMenu.Empty>
<CommandMenu.Loading icon={<Spinner />}>Searching…</CommandMenu.Loading>

In this example, the “Deals” results resolve after a delay, so typing shows the Loading row before the results appear: