Command palette
StableSearch and commands in one place. It grows out of the search pill in the header, finds pages, actions and records as the merchant types, and folds back into the pill when they are done.
Basics
A SearchTrigger in the header opens a CommandPalette. Inside, a CommandGroup holds CommandItems under a heading. Pages and quick actions are filtered right here as you type; orders, products and customers come from a (simulated) server half a second later, while a comet runs along the field.
Open the search pill and try "whit", "1042" or "leeds".
In both themes
Held open for review, light above dark: a search for "whit" with the first order selected. The panel is 640px wide with 20px corners on the modal shadow. The selected row takes a hover tint, its icon tile turns teal, and a short teal mark on the rim marks it.
Open them for real
Each pill opens the palette in the theme of its own side, and it grows out of that pill.
Growing out of the pill
Opened from a SearchTrigger, the palette grows out of it: the pill stretches into the panel as it glides to its place near the top of the window, the pill's words carry over into the field, and the results fade up behind them. The pill leaves the header while the palette is out. On closing, the panel folds back into the pill, a little faster and without a wobble.
The words hand straight over when the palette's placeholder matches the pill's, which it does by default. origin grows it out of any other element, such as a round search button in a phone header, and origin={null} always scales it up from the centre. With no visible pill, or with reduced motion, it scales up from the centre or crossfades.
Open it three ways, then close it with Esc to watch it fold back.
Search trigger
The pill in the header. It looks like a field but is a button: pressing it opens the palette. It prefers 360px and shrinks with its container, dropping the keycaps first (below 260px) and then its words (below 96px), leaving a round search button. Its words become its accessible name, so it stays named when they are hidden.
Items
A CommandItem is one result. icon sits in a rounded tile that turns teal when the row is selected; pass a Phosphor icon, or initials for a person. hint is quiet text at the end, such as a price or a status, and shortcut shows keycaps exactly as written. keywords are matched without being shown, like an SKU or an email. value must be unique in the palette; it defaults to the item's text.
Try typing an SKU (CHR-ARD) or an email (whitfield).
| Part | What it does |
|---|---|
CommandGroup | A titled run of items. Headings are sentence case. A group hides itself when none of its items match. |
CommandSeparator | A hairline between groups. Hidden while there is a search, unless alwaysRender. |
CommandKey | A keycap, as used in item shortcuts and the footer. Use it in your own footer. |
CommandHints | The default footer: arrows to move, Enter to open, Esc to close. Hidden on touch screens. |
Loading and empty
While results are on their way, loading sends a comet along the bottom of the field and the list is marked busy. Show a CommandLoading row while there is nothing else to show. Once results have arrived, CommandEmpty appears by itself when nothing matches: an empty orbit, a line saying so and a description of what to try. Render it only when loading is done, or it flashes up before the results.
Keyboard
The palette is built to be driven without the mouse.
| Keys | What happens |
|---|---|
| ⌘K or CtrlK | Opens it from anywhere on the page (hotkey on the SearchTrigger, on by default), and closes it again from inside. |
| Typing | Filters and ranks the items as you type, matching their text and keywords loosely. |
| ↑↓ | Move the selection, wrapping round at the ends (loop). |
| ↵ | Runs the selected item's onSelect. |
| esc | Closes it. So does the Esc keycap in the field. |
- When it opens, focus goes straight to the field. When it closes, focus goes back to where it was, usually the pill.
- Put the most likely destinations first, and keep the order stable so people learn where things are.
- Name items as destinations or actions: "Orders", "Create product". Put the detail in the hint.
Accessibility
- The palette is a Radix dialog: focus stays inside, the page behind cannot scroll and is hidden from screen readers. It is named by
label("Search" by default), and its list of results is named "Search results". - The field is a combobox and the results are a listbox of options, so screen readers announce the selected row as the arrows move (via cmdk).
- While
loading, the list is markedaria-busy, andCommandLoadingis a progress bar named by its text. SearchTriggeris a real button named by its words, witharia-haspopup="dialog"and, while the hotkey is on,aria-keyshortcuts.- With reduced motion there is no morph: the palette crossfades, the loading comet parks and breathes, and the selection mark jumps instead of sliding.
Props
CommandHints takes no props. Items, groups, empty and loading also accept the matching cmdk props.
CommandPalette
| Prop | Type | Default | Description |
|---|---|---|---|
openRequired | boolean | None | Controlled open state. |
onOpenChangeRequired | (open: boolean) => void | None | Called when it opens or closes. |
value | string | None | The query, controlled. Use it with onValueChange to search a server. |
onValueChange | (query: string) => void | None | Called with the query as it changes. |
loading | boolean | false | Sends the comet along the field and marks the list busy. |
placeholder | string | None | The field's placeholder. Defaults to the words of the pill it grows out of. |
shouldFilter | boolean | true | Set to false when a server does the filtering. |
filter | (value, search, keywords?) => number | None | Custom ranking: 0 hides an item, 1 is a perfect match. |
loop | boolean | true | The arrow keys wrap round the list. |
footer | ReactNode | None | Footer content. Defaults to CommandHints; pass null for no footer. |
label | string | 'Search' | Accessible name of the dialog and its list. |
origin | HTMLElement | RefObject<HTMLElement> | null | None | What it grows out of. Defaults to the SearchTrigger that opened it (or any visible one); null scales it up from the centre. |
container | HTMLElement | null | document.body | Where the palette is portalled. |
CommandGroup
| Prop | Type | Default | Description |
|---|---|---|---|
heading | ReactNode | None | The group title, in sentence case. |
value | string | None | A unique value for the group. Needed when there is no heading. |
forceMount | boolean | false | Show it whatever the search. |
CommandItem
| Prop | Type | Default | Description |
|---|---|---|---|
childrenRequired | ReactNode | None | The label. |
onSelect | (value: string) => void | None | Runs on Enter or a click. |
value | string | None | Unique in the palette. Defaults to the text of children. |
keywords | string[] | None | Matched while searching but not shown, e.g. an SKU or an email. |
icon | ReactNode | None | Shown in a rounded tile at the start. An icon, or initials. |
hint | ReactNode | None | Quiet trailing text, e.g. a price or "12 in stock". |
shortcut | string[] | None | Keycaps at the end, shown as written, e.g. ['G', 'O']. |
disabled | boolean | false | Dimmed, and cannot be selected. |
forceMount | boolean | false | Show it whatever the search. |
CommandEmpty
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | 'No results' | The line saying nothing matched. |
description | ReactNode | None | A quieter second line: what to try instead. |
icon | ReactNode | None | Replaces the empty-orbit glyph. Pass null for none. |
CommandLoading
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | 'Searching' | What is being loaded, beside an orbit spinner. |
label | string | None | Accessible name of the progress bar. Defaults to the text of children. |
progress | number | None | How far along the load is, when you know. |
CommandSeparator
| Prop | Type | Default | Description |
|---|---|---|---|
alwaysRender | boolean | false | Show it while there is a search too. |
CommandKey
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | None | The key, e.g. "#" or "↵". Accepts every kbd attribute. |
SearchTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
onOpen | () => void | None | Called on a press and on the hotkey. Open the palette here. |
placeholder | string | 'Search orders, products, customers' | The words in the pill, and its accessible name. The palette takes them as its placeholder. |
hotkey | boolean | true | Listen for ⌘K (Ctrl K elsewhere) anywhere on the page. |
shortcut | boolean | true | Show the ⌘ K or Ctrl K keycaps. |
width | number | string | 360 | Preferred width in px or any CSS length. It shrinks with its container. |
disabled | boolean | false | Native disabled. Also accepts every other button attribute. |