# Command Palette

> A search field over your commands, with nested pages that slide in and a highlight that glides between rows.

- Page: https://ui.devigner.cc/components/command-palette
- Category: Interface
- Requirements: React 18 or 19, Tailwind CSS v4
- Install: `npx devignerui add command-palette` (writes components/ui/command-palette.tsx)
- Packages it needs: cn, motion, @devigner-ui/icons, devignerui

## What the demo shows

The live demo is a palette with suggestions and settings. Type to filter every page at once, move with the arrow keys, and open Change theme to see the next page slide in with its name as a crumb in the field.

## When to use it

- A Cmd+K or Ctrl+K launcher for an app or docs site: jump to pages, run actions, change settings.
- Long command lists that benefit from search, grouped into sections, with deeper pages for choices like theme or language.

## When to reach for something else

- Picking one value for a form field. Use a select or combobox, which keeps the choice visible in the field.
- Primary navigation people need to see without typing. Use visible links or tabs.

## Usage

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

<CommandPalette items={commands} />
```

Custom composition:

```tsx
<CommandPalette items={commands} autoFocus hints={false} onSelect={() => setOpen(false)} messages={{ placeholder: "Type a command" }} />
```

### Open it with Cmd+K

A native dialog that opens on Cmd+K or Ctrl+K, focuses the field and closes after a command runs.

```tsx
import { useEffect, useRef } from "react";
import { CommandPalette, type CommandPaletteItem } from "@/components/ui/command-palette";

export function CommandMenu({ items }: { items: CommandPaletteItem[] }) {
  const dialog = useRef<HTMLDialogElement>(null);
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key.toLowerCase() === "k" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        dialog.current?.showModal();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);
  return (
    <dialog ref={dialog} className="m-auto bg-transparent p-0 backdrop:bg-black/40">
      <CommandPalette items={items} autoFocus onSelect={() => dialog.current?.close()} />
    </dialog>
  );
}
```

### Nested pages with groups and shortcuts

Rows sharing a group sit under one heading; a row with items opens a page.

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

const items: CommandPaletteItem[] = [
  { label: "New file", group: "Files", shortcut: ["Ctrl", "N"], onSelect: () => console.log("new") },
  { label: "Open recent", group: "Files", keywords: ["history"], onSelect: () => console.log("recent") },
  {
    label: "Change theme",
    group: "Settings",
    items: [
      { label: "Light", onSelect: () => console.log("light") },
      { label: "Dark", keywords: ["night"], onSelect: () => console.log("dark") },
    ],
  },
];

export function Example() {
  return <CommandPalette items={items} />;
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| items | `CommandPaletteItem[]` | - | The rows: { label, id?, icon?, description?, action?, group?, keywords?, shortcut?, items?, onSelect?, disabled? }. A row with items opens a page of its own; description adds a muted second line, and action is the word shown on the active row. |
| onSelect | `(item) => void` | - | Called for every chosen row that is not a page, after the row's own onSelect. |
| query | `string` | - | Controlled search text. Pass with onQueryChange. |
| defaultQuery | `string` | "" | Search text on first render when uncontrolled. |
| onQueryChange | `(query: string) => void` | - | Called as the search text changes. |
| path | `number[]` | - | Controlled page as an index path: [] is the top, [3] the page row 3 opened. Pass with onPathChange. |
| defaultPath | `number[]` | [] | Page on first render when uncontrolled. |
| onPathChange | `(path: number[]) => void` | - | Called when a page opens or closes. |
| resetOnSelect | `boolean` | true | After a row runs, clears the search and goes back to the top page. |
| hints | `boolean` | true | Shows the Move, Open and Back key hints under the list. |
| label | `string` | Command palette | Accessible name of the search field. |
| autoFocus | `boolean` | false | Focuses the search field on mount, for a palette that opens in a dialog. |
| onEscape | `() => void` | - | Called on an Escape with no search to clear and no page to leave, and shows an Esc cap in the field. Close a dialog from here. |
| groupActions | `Record<string, CommandPaletteGroupAction>` | - | A button at the end of a group heading, keyed by group name: { label, onSelect }, such as Clear on a Recent group. |
| messages | `Partial<CommandPaletteMessages>` | - | Replaces any built-in string: the placeholder, the empty states, the back button name and the hints. |
| classNames | `CommandPaletteClassNames` | - | Slot classes: root, field, list, item, indicator (the gliding highlight) and footer. |

## Keyboard

| Keys | Action |
| --- | --- |
| Up / Down arrows | Move the highlight, skipping disabled rows, looping at the ends. |
| Enter | Runs the highlighted row, or opens its page. |
| Backspace | On an empty field, goes back one page. |
| Escape | Clears the search, then goes back a page. On the top page with an empty field it calls onEscape, so a dialog around the palette can close. |

## Accessibility

- The field is a combobox controlling a listbox, and the highlighted row is announced through aria-activedescendant, so focus never leaves the field.
- Sections are groups named by their heading, rows with a page carry aria-haspopup, and disabled rows carry aria-disabled.
- An empty search is a status message that names the query.
- The crumb in the field is a button named "Back from <page>" by default.
- Honors prefers-reduced-motion and an ancestor <MotionConfig reducedMotion="always">, so an in-app motion switch works too.

## Theming

- classNames targets root, field, list, item, indicator and footer.
- Colors come from shadcn tokens: the highlight is foreground at 4.5% (7% in dark), matches and the crumb are primary. Idle rows are muted-foreground and the active one foreground. The surface is var(--panel) when your app defines it and background otherwise, with a 4% foreground rim and a layered shadow. Key caps and icon tiles are inset wells on background/40.
- messages replaces the placeholder, both empty states, the crumb's name, the hint labels and the default action word.
- shortcut shows key caps at the end of a row. They are display only; bind the keys yourself.
- The active row shows its action word in primary at the end (messages.run, Run, when the item sets none). A row with a shortcut keeps its keys instead.
- Group headings and their actions are set in the mono font, rows at 13px; the icon darkens on the active row.

## Edge cases

- A search covers the current page and every page under it. Results show the pages they sit in, and drop section headings so the best match comes first.
- Choosing a row runs its onSelect, then the palette's onSelect, then returns to the top page with an empty field. Set resetOnSelect to false to stay put.
- A row with both items and onSelect opens its page; onSelect is not called.
- The search matches a row's description as well as its label and keywords.
- The pointer lights the nearest row, so a heading or a gap between rows never drops the highlight. Each time the pointer enters the list, the highlight fades in under it rather than sliding over from the row the keys left it on.
- A group action button keeps focus in the field when clicked, so the arrow keys still work after it runs.
- The list scrolls past 384px, keeping the highlighted row in view without scrolling the page.
- The palette is inline. Put it in your own dialog or popover to open it with Cmd+K, as in the recipe.

## Troubleshooting

**Escape closes my dialog while there is still text in the field.**
It shouldn't: the palette stops Escape while it has a search or a page to undo. If your dialog listens on window in the capture phase, listen in the bubble phase instead.

**The palette goes back to the top after every choice.**
That is resetOnSelect, on by default. Pass resetOnSelect={false} to keep the page and search.

**The component renders with no background or the wrong colors.**
Colors use the standard shadcn/ui tokens only (background, foreground, primary, secondary, muted, accent, border, input, ring, destructive), so a project set up with shadcn/ui needs nothing extra and the component follows its theme, dark mode included. Without shadcn/ui, define those CSS variables for :root and .dark and map them in your Tailwind v4 @theme, or run npx shadcn init.
