Skip to main content

CommandPalette

A command palette is a search box over everything a user can do. This one is a modal Dialog holding a Headless UI Combobox: the dialog gives the focus trap, scroll lock and Escape; the combobox gives typeahead, arrow keys and role="listbox" semantics. It never holds a value: choosing an item runs it and closes the palette. Below sm (40rem) it is a full-width sheet pinned to the top of the viewport, whatever its size.

import CommandPalette from '@zuilib/primitives/command-palette'

Basic

open / onOpenChange follow the Dialog contract. Items carry an onSelect or an href; group sorts them under the headings you list in groups. Selected items lead the list under Recent the next time it opens.

Loading example

Recent items

The ids of chosen items are kept most-recent-first, capped at maxRecent (5). Pass storageKey to persist them in localStorage, or control the list with recentIds / onRecentIdsChange (a per-user store). With an empty query the recent items are listed first and left out of their own groups; typing hides the recent group.

<CommandPalette storageKey="app.commands" maxRecent={3} {...rest} />

Async results

filter={false} turns the built-in matching off and renders items as given, so onQueryChange can drive a server search; loading swaps the list for a spinner row while it runs.

const [query, setQuery] = useState('')
const {data, isFetching} = useSearch(query)

<CommandPalette
items={data ?? []}
filter={false}
onQueryChange={setQuery}
loading={isFetching}
{...rest}
/>

Props

CommandPaletteProps extends the <div> attributes (they land on the dialog root) with:

PropTypeDefaultDescription
openbooleanControlled visibility; use defaultOpen for uncontrolled
defaultOpenbooleanfalseUncontrolled initial visibility
onOpenChange(open: boolean) => voidFalse on Escape, backdrop click, a selection and the shortcut while open; true from the shortcut while closed. Flip open in response
itemsrequiredCommandPaletteItem[]{id, label, description?, icon?, shortcut?, keywords?, group?, href?, onSelect?, disabled?}
groups{id: string; label: string}[]Order and headings of the groups; ungrouped items come first, unknown groups are appended headed by their id
onSelect(item) => voidEvery selection, before the item's own onSelect
onNavigate(href, item) => voidwindow.location.assignFor items with href; pass your router's navigate
shortcutstringGlobal keyboard shortcut that toggles the palette: 'mod+k' (mod is Meta or Ctrl), 'ctrl+shift+p', '/'. A shortcut without Ctrl/Meta/Alt is ignored while a text field has focus. Off by default
placeholderstring'Type a command or search…'Labels.commandPalettePlaceholder
emptyMessageReactNode'No results found.'Labels.noResults
loadingbooleanfalseSpinner row instead of the list
loadingMessageReactNode'Loading…'Labels.loadingMessage
onQueryChange(query: string) => voidEvery keystroke, and '' when the palette closes
filterfalse | (item, query) => booleanlabel, description and keywords, case-insensitivefalse renders items as-is
recentIdsstring[]Controlled recent ids, most recent first
onRecentIdsChange(ids: string[]) => void
storageKeystringPersist the recent ids in localStorage under this key. Ignored while recentIds is controlled; changing the key re-reads storage
maxRecentnumber5
recentLabelstring'Recent'Labels.recent
labelstring'Command palette'Accessible name of the dialog and the input; Labels.commandPalette
size'sm' | 'md' | 'lg' | 'xl' | 'full''lg'Dialog size
role'dialog' | 'alertdialog''dialog'
trackstringNames the select telemetry event (value is the item id)
classNamestringMerged last onto the panel
childrenReactNodeRendered in a footer strip under the list

Slots

SlotElementNotes
[data-slot="command-palette"]divThe dialog root (see Dialog for its attributes); print:hidden
[data-slot="command-palette-panel"]divThe dialog panel with --dialog-padding zeroed
[data-slot="command-palette-header"]divSearch icon + input row
[data-slot="command-palette-input"]inputrole="combobox", focused on open
[data-slot="command-palette-list"]divrole="listbox", scrolls past 80 spacing units
[data-slot="command-palette-group"]divrole="group" labelled by its heading (none for ungrouped items)
[data-slot="command-palette-heading"]div
[data-slot="command-palette-item"]divrole="option" with data-item-id; Headless UI data-focus / data-disabled
[data-slot="command-palette-item-icon"]spanaria-hidden
[data-slot="command-palette-item-content"]spanLabel and description column
[data-slot="command-palette-item-description"]span
[data-slot="command-palette-item-shortcut"]kbdaria-hidden
[data-slot="command-palette-loading"]divSpinner row while loading
[data-slot="command-palette-empty"]divThe emptyMessage row
[data-slot="command-palette-status"]spanVisually hidden live region for loading / empty text
[data-slot="command-palette-footer"]divPresent when children are given

Tokens

TokenUsed for
--dialog-width-lg (or the chosen size)Panel width
--control-height-lg, --control-padding-x-mdInput height, row padding
--popover, --popover-foreground, --borderPanel surface, header and footer rules
--accent, --accent-foregroundThe focused row
--muted-foregroundHeadings, descriptions, shortcuts, the search icon

Accessibility

  • The dialog is role="dialog" with aria-modal="true" and named by label (Command palette); focus lands in the input, is trapped with Tab / Shift+Tab, and returns to the opener on close.
  • The input is role="combobox" with aria-expanded, aria-controls and aria-activedescendant; the list is role="listbox", each group role="group" labelled by its heading, each row role="option" with aria-selected and aria-disabled.
  • ArrowUp / ArrowDown move over the enabled options (the first is active on open), Enter runs the active one, Escape closes. The shortcut is ignored while a keydown was already handled (defaultPrevented), and a shortcut with no Ctrl/Meta/Alt (such as '/') is ignored while an input, textarea or contenteditable element has focus.
  • Loading and empty rows are disabled options and are also announced from the command-palette-status live region.
  • Shortcut hints are decorative (aria-hidden); bind the keys yourself.
  • Dialog: the modal shell.
  • Combobox: the same list semantics as a form field.
  • Menu: a short action list on a button.
  • Kbd: the shortcut chip for the trigger.