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 theItemsrender param with that type, since like Base UI’sAutocomplete, it isn’t inferred fromRoot’sitems. - Filtering: applied to the static
itemsinternally. Rows match onsearchValue, the group label, and anycmdk-stylekeywords(e.g. a company, domain, or email): related info that isn’t part of the visible label. - Selection: Base UI’s
onClickonItem; fires on pointer click and onEnterwhen the row is highlighted. - Warm on intent:
Root’sonItemHighlighted(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: