<!-- Protocore Design System — CommandPalette -->
# CommandPalette

- **Category:** Overlay (`overlay`)
- **Slug:** `overlay/command-palette`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { CommandPalette } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/overlay/command-palette

> A ⌘K spotlight finder — grouped, filterable commands driven entirely from the keyboard over a dimmed backdrop.

## When to use it

A CommandPalette is the **keyboard-first launcher** for a dense app: navigation, actions, and settings unified behind ⌘K and fuzzy search. It rewards power users and scales to hundreds of commands where a visible menu can't. Group commands by domain (Navigate, Deploy, Settings) and add `keywords` so a command surfaces under the words people actually type.

It is not a form control — for selecting a value from options with typeahead, use a **[Combobox](/inputs/combobox)** or **[Select](/inputs/select)**. For a small, fixed action list off a button, a **[DropdownMenu](/overlay/dropdown-menu)** is simpler. Reserve the palette for the app-wide command surface, and always keep a visible entry point (a search affordance or hint) so the shortcut is discoverable.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `bindShortcut` | `boolean` | no | `false` | Register a global ⌘K / Ctrl+K listener that toggles the palette. |
| `className` | `string` | no | — | Merged after the pds class on the panel. |
| `defaultOpen` | `boolean` | no | — | Initial open state in uncontrolled mode. |
| `emptyMessage` | `string` | no | `No results` | Copy shown when nothing matches the query. |
| `groups` | `CommandGroup[]` | yes | — | The command catalog, grouped. Empty groups (after filtering) are hidden. |
| `onOpenChange` | `((open: boolean) => void)` | no | — | Fires when the palette requests open/close (⌘K, Esc, backdrop, selection). |
| `open` | `boolean` | no | — | Controlled open state. |
| `placeholder` | `string` | no | `Search commands…` | Search input placeholder. |

## Examples

### Basics

Feed `groups` of commands — each item has an `id`, `label`, optional `icon` / `hint` / `keywords`, and an `onSelect` handler. Set `bindShortcut` to register a global ⌘K / Ctrl+K toggle. Type to filter across labels, hints, group names, and keywords; the palette closes before running the choice.

```tsx
import { useState } from "react";
import { CommandPalette, Button, Text, VStack } from "@protocore/pds";
import { GitBranch, Play, RefreshCw, Search, Settings, Terminal } from "lucide-react";

export default function CommandPaletteBasics() {
  const [open, setOpen] = useState(false);
  const [last, setLast] = useState<string | null>(null);
  const run = (label: string) => () => setLast(label);

  return (
    <VStack gap={3} align="start">
      <Button variant="secondary" onClick={() => setOpen(true)}>
        Open palette (⌘K)
      </Button>

      <CommandPalette
        open={open}
        onOpenChange={setOpen}
        bindShortcut
        placeholder="Search commands…"
        groups={[
          {
            label: "Navigate",
            items: [
              {
                id: "logs",
                label: "Go to logs",
                icon: <Terminal size={15} />,
                onSelect: run("Go to logs"),
              },
              {
                id: "search",
                label: "Search ledger",
                icon: <Search size={15} />,
                hint: "⌘/",
                onSelect: run("Search ledger"),
              },
            ],
          },
          {
            label: "Deploy",
            items: [
              {
                id: "deploy",
                label: "Deploy main",
                icon: <Play size={15} />,
                hint: "⌘↵",
                onSelect: run("Deploy main"),
              },
              {
                id: "branch",
                label: "Deploy branch",
                icon: <GitBranch size={15} />,
                keywords: ["release"],
                onSelect: run("Deploy branch"),
              },
              {
                id: "resync",
                label: "Re-sync validators",
                icon: <RefreshCw size={15} />,
                onSelect: run("Re-sync validators"),
              },
            ],
          },
          {
            label: "Settings",
            items: [
              {
                id: "prefs",
                label: "Open preferences",
                icon: <Settings size={15} />,
                hint: "⌘,",
                onSelect: run("Open preferences"),
              },
            ],
          },
        ]}
      />

      <Text size="sm" color="muted">
        {last ? `Ran: ${last}` : "Press ⌘K or the button, then type to filter."}
      </Text>
    </VStack>
  );
}
```

### Global ⌘K, no button

In uncontrolled mode with `bindShortcut`, ⌘K opens the palette from anywhere — no trigger element required. `emptyMessage` customizes the no-results copy.

```tsx
import { CommandPalette, Text, VStack } from "@protocore/pds";
import { Copy, FileText, Moon, Sun } from "lucide-react";

export default function CommandPaletteUncontrolled() {
  const noop = () => {};

  return (
    <VStack gap={3} align="start">
      <Text size="sm" color="secondary">
        With <code>bindShortcut</code>, ⌘K / Ctrl+K toggles the palette globally — no trigger button
        needed.
      </Text>

      <CommandPalette
        bindShortcut
        emptyMessage="No matching commands"
        groups={[
          {
            label: "Document",
            items: [
              {
                id: "new",
                label: "New snapshot",
                icon: <FileText size={15} />,
                hint: "⌘N",
                onSelect: noop,
              },
              { id: "dup", label: "Duplicate config", icon: <Copy size={15} />, onSelect: noop },
            ],
          },
          {
            label: "Appearance",
            items: [
              { id: "light", label: "Light theme", icon: <Sun size={15} />, onSelect: noop },
              { id: "dark", label: "Dark theme", icon: <Moon size={15} />, onSelect: noop },
            ],
          },
        ]}
      />
    </VStack>
  );
}
```

## Do & don't

**Do**

- Group commands by domain and label each group.
- Add keywords so commands match the terms users actually type.
- Enable bindShortcut for a global ⌘K and show a visible hint of it.
- Put frequent shortcuts in each item's hint (⌘S, ⌘↵).

**Don't**

- Use it as a form field — that's a Combobox or Select.
- Hide it behind a shortcut with no on-screen affordance to reveal it exists.
- Dump every command flat with no grouping — filtering gets noisy.
- Duplicate ids across groups; each command id must be unique.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `⌘K / Ctrl+K` | Toggles the palette when bindShortcut is enabled. |
| `↑ / ↓` | Moves the active command; the list wraps at the ends. |
| `Home / End` | Jumps to the first / last command. |
| `Enter` | Runs the active command and closes the palette. |
| `Esc` | Closes the palette without running anything. |

**Notes**

- Focus stays on the search input; the results are a role=listbox driven by aria-activedescendant.
- The active option is announced via aria-activedescendant as you arrow through results.
- Built on Radix Dialog: modal backdrop, focus trapped, focus restored on close.
- Typing re-filters instantly and resets the active row to the first match.

## Related

`dialog`, `search-input`, `dropdown-menu`, `kbd`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
