# Command Palette (/docs/command-palette)

A searchable keyboard-first command surface composed from 4AF primitives.

## Install

```bash
npx shadcn@latest add https://4af.selfsimilar.dev/r/command-palette.json
```

## Usage

```tsx
import { CommandPalette } from "@/components/ui/command-palette";

<CommandPalette groups={groups} />
```

## API

**Main exports:** `CommandPalette`, custom panel, list, row, tab and action parts, `Kbd`,
`useCommandSearch`, `useCommandPanelHighlight`

A keyboard-first command and search dialog with fuzzy ranking, groups, nested and custom panels,
async, server-driven, and paginated search, header text fields, per-panel header actions, badges,
checkboxes, keybinds, and destructive actions.

`loader={<MySpinner />}` supplies the palette's search, panel, and action indicators. Omit it to
inherit [LoaderProvider](/docs/reference/engineering#shared-loading-indicators), with `AsciiLoader` as
the fallback. `CommandItem` data and `CommandPanelLoading` accept their own `loader`; a custom
`icons.pending` takes precedence for action rows.

`shortcut` is on by default and binds `⌘K` / `Ctrl+K` to toggle the palette. The demo on this
page uses `⌘⇧P` / `Ctrl+Shift+P` instead, so it doesn't clash with the site search.

`useCommandSearch` discards results from cancelled requests, even when the search callback
ignores its abort signal. Clearing the query or disabling search resets the results, and a late
response cannot bring them back or appear during the next query's debounce.

Root `onSearch(query, signal)` returns `Promise<CommandGroup[]>`; returned order is preserved.
Use `searchDebounce` and `loadingMessage` to configure waiting behavior. In custom panels,
render the initial loader while `searching && results === null`.

Header navigation buttons use the compact 6px radius.

## Composition

The palette enters and exits like Dialog: the backdrop fades, the panel scales in from a small
blur, and the exit fades and blurs without reversing the entrance. Reduced motion skips the
transition.

The registry item includes:

- root groups and rows with icons, descriptions, badges, keybinds, and destructive state;
- nested item lists;
- arbitrary custom panels opened through `helpers.openPanel`;
- custom panel header search fields or traditional text fields;
- Escape-to-clear for `type="search"` header fields, while traditional text fields use Escape
  to go back;
- per-panel `headerActions`, suitable for Select and Select Sort controls—use the default
  `size="sm"` so they fit the palette;
- custom list rows with trailing badges or selection checkboxes;
- grouped action panels with section labels, icons, keybinds, search, and destructive actions;
- async or server-driven search: pass `onSearch` on the root with `groups` as the resting state,
  or call `useCommandSearch` in a custom panel and render `CommandPanelLoading` for its first-load
  state. Root failures render `searchErrorMessage` (default: “Search failed. Change your query to
  retry.”); custom panels receive the failure as the hook's `error`;
- paginated custom panels: observe the shared keyboard and pointer highlight with
  `useCommandPanelHighlight`, fetch when the last row is reached, and pass that request as
  `additionalRefreshing` to `useCommandSearch` so the panel header keeps showing its inline
  loader;
- a tab row directly under a custom panel's header, with `CommandPanelTabs`;
- opening the palette straight onto one page with `initialItemId`.

The demo's Projects, Rename Project, New Project, and Keyboard Shortcuts flows are built only
from these public APIs.

## Panel tabs

A custom panel can put a row of tabs directly under its header. `CommandPanelTabs` is a Base UI
Tabs root that wraps everything below the header — the row, the body, and the footer — so arrow
keys, Home/End, and the tab/panel wiring come from the primitive. `CommandPanelTabList` draws the
row and its one travelling indicator; `variant` picks how the selected tab is marked:

| `variant` | Selected tab |
| --- | --- |
| `fill` (default) | A muted fill slides behind it |
| `underline` | A hairline slides along the rule under the row |

```tsx
<CommandPanelTabs value={tab} onValueChange={setTab}>
  <CommandPanelTabList aria-label="Sections" variant="underline">
    <CommandPanelTab value="all">All</CommandPanelTab>
    <CommandPanelTab value="recent">Recent</CommandPanelTab>
  </CommandPanelTabList>
  <CommandPanelTabPanel value={tab}>
    <CommandPanelBody>…</CommandPanelBody>
  </CommandPanelTabPanel>
  <CommandPanelFooter />
</CommandPanelTabs>
```

Both variants sit on the header's bottom rule. Tabs activate as the arrow keys reach them
(`activateOnFocus` is on by default here). When the tabs filter one list, a single
`CommandPanelTabPanel` whose `value` follows the selection is enough. A header search field
filters within the selected tab.

Within a custom panel using these tabs, Tab selects the next enabled tab and Shift+Tab the
previous one, wrapping at either end and skipping disabled tabs. Focus stays where it is,
so the header search field keeps its query and you can continue typing after switching.
Panels without a tab bar keep ordinary Tab focus navigation; open menus keep their own keys.

The indicator animates between the selected tabs.

Use these panel tab parts instead of nesting the standalone `Tabs` component.

## Opening on a page

`initialItemId` opens the palette straight into one item's custom panel or sub-list instead of the
root list, for a shortcut that opens one page, such as `?` for a help panel. It is read each time
the palette opens. From that page, Escape and Back close the palette instead of showing the root
list; pages opened from the root still step back as usual.

```tsx
<CommandPalette groups={groups} open={open} onOpenChange={setOpen} initialItemId={entry} />
```

## Selection and hover

Every list — the root list and each custom panel's — opens with its first row selected, before
anything is typed, and moves the selection to the new first row as a query narrows the list.
Enter runs the selected row without an arrow press first.

Selection and hover are separate states. The selected row carries `data-highlighted` and moves
with the arrow keys and with presses. Hovering gives a row a weaker fill only, so moving the
pointer across the list never changes what Enter runs. A press selects a row and runs it, so in a
panel that stays open, such as a checkbox list, the selection follows the row you pressed.
Moving the pointer out of the list leaves the selection where it was.

While a row's actions menu is open, the row carries `data-frozen`, styled like the selection,
because Base UI drops its own highlight while focus is inside the menu.

When the selection reaches the first row, the list scrolls to its top, so the section label above
that row is visible. This applies to the root list and to `CommandPanelList`.

The root list primes its first row from the `items` collection through `autoHighlight`.
Custom panels have no such collection, so `CommandPanelList` activates its first row after
mount. Preserve that initialization when changing panel composition; root highlighting does
not initialize custom panels.

The no-results state is the height of one section label plus one row with a description.

## Panel footer

The root list's bar has the escape hint and the navigation hint on the left, and the Enter
hint pinned right. A custom panel's footer keeps those two ends — escape on the left, Enter on
the right — and fills the right corner with two optional slots, in this order:

| Slot | Prop | Key |
| --- | --- | --- |
| The primary action | `enter` | Return, fixed |
| The contextual actions menu | `actions` | Cmd/Ctrl + J, fixed |

```tsx
<CommandPanelFooter
  enter={{ label: "Save", type: "submit" }}
  actions={{ groups: actionGroups }}
/>
```

Neither key is configurable, and nothing else can go in that corner, so every panel uses the
same keys. `enter` takes `label`, `onClick`, `disabled`, and `type`. `"submit"` submits the containing
form; an `onClick` handler, if provided, also runs. `actions` takes the menu's own props:
`items` or `groups`, plus `label`, `searchPlaceholder`, and `emptyMessage`.

An actions row normally runs `onSelect` and closes the menu. The fields below make it show its
result in place instead, keeping the menu open. They match Dropdown Menu's row types:

| Shape | Fields | Dropdown equivalent |
| --- | --- | --- |
| Runs, confirms, reverts | `action` | `DropdownMenuActionItem` |
| Runs, then lands checked | `action` + `checked`/`defaultChecked` | `DropdownMenuCheckboxItem` with `action` |
| Flips at once, morphing | `checked`/`defaultChecked` | `DropdownMenuCheckboxItem` + `Morph` |
| Asks, then commits and closes | `confirm` + `onSelect` | `DeleteButton` |

```tsx
<CommandPanelFooter
  actions={{
    items: [
      { id: "sync", label: "Sync now", action: sync, labels: { pending: "Syncing…", success: "Synced" } },
      { id: "watch", label: "Watch", defaultChecked: false, action: watch, labels: { checked: "Watching" } },
      { id: "star", label: "Star", defaultChecked: false, labels: { checked: "Starred" } },
    ],
  }}
/>
```

`labels` and `icons` carry the other states—checked, pending, success, and error—each falling back
to the row's own label or icon. Pending and error default to the shared loader and warning
triangle. `revertAfter` and `pendingDelay` behave as on Action Button. The two toggle shapes show
no success tick; the changed label is the confirmation. An item with none of these fields closes
the menu and calls `onSelect`.

`confirm` requires two presses: the first arms the row and shows `confirm.label`, “Confirm?”
by default; the second runs `onSelect` and closes the menu. It disarms on Escape, when the
menu closes, and after `confirm.resetAfter`—three seconds by default, while `0` means never. A press within 350ms of arming is ignored, so one
double-click cannot confirm what it just armed. Delete Button follows the same rules.