# CommandPalette

**When to use:** A search-first dialog for running any command or jumping anywhere in the app from the keyboard, usually opened with ⌘K / Ctrl+K. It takes the same `MenuEntry[]` as [Menu](Menu.md), so one list can feed the menus, the palette and the shortcuts. For picking a value in a form, use [InputDropdown](InputDropdown.md).

**Keywords:** cmd+k, ctrl+k, command menu, quick open, launcher, spotlight, go to anything, jump to, omnibox

**Import:** `import { CommandPalette } from '@ahrowe/ui'`  
**Types:** `import type { CommandPaletteProps, CommandPaletteLabels } from '@ahrowe/ui'`

```tsx
import { CommandPalette, menuHotkeys, useHotkeys } from '@ahrowe/ui';
import type { MenuEntry } from '@ahrowe/ui';

const commands: MenuEntry[] = [
  { heading: 'Invoice' },
  { id: 'new-invoice', label: 'New invoice', icon: faPlus, shortcut: 'mod+n', onClick: createInvoice },
  { id: 'export', label: 'Export as PDF', icon: faFilePdf, keywords: ['download', 'print'], onClick: exportPdf },
  { heading: 'Go to' },
  { id: 'customers', label: 'Customers', onClick: () => navigate('/customers') },
];

function App() {
  const [isOpen, setOpen] = useState(false);
  useHotkeys({ ...menuHotkeys(commands), 'mod+k': () => setOpen(true) });

  return <CommandPalette items={commands} isOpen={isOpen} onClose={() => setOpen(false)} />;
}
```

**Opening it is yours.** The palette binds no shortcut of its own, for the same reason `Menu` does not: if it listened for ⌘K and your app did too, it would open twice. `menuHotkeys` binds each command's `shortcut`, so the hint the palette draws and the key that fires stay one spelling.

**Filtering** matches every typed word, in any order, against a string `label` and the command's `keywords`, ignoring case and accents: `uber` finds `Übersicht`. Within each group, a label starting with the query comes first, then a label word or a keyword starting with it, then any other match, and the group holding the best match moves to the top. A heading moves with its group and stays above it while any of it matches; separators are dropped while a query is typed. Typing highlights the best match, so Enter runs it. A `label` that is not a string cannot be read, so give such a command `keywords`.

**Server results.** Set `filter={false}` and the palette shows `items` exactly as given, so they can be the results of your own search:

```tsx
const [results, setResults] = useState<MenuEntry[]>(commands);
const [isLoading, setLoading] = useState(false);

<CommandPalette
  items={results}
  filter={false}
  isLoading={isLoading}
  onQueryChange={(query) => {
    if (!query) return setResults(commands);
    setLoading(true);
    searchCustomers(query).then((hits) => {
      setResults(hits.map((c) => ({ id: c.id, label: c.name, onClick: () => navigate(`/customers/${c.id}`) })));
      setLoading(false);
    });
  }}
  isOpen={isOpen}
  onClose={() => setOpen(false)}
/>;
```

`onQueryChange` fires on every keystroke and with `''` each time the palette opens, so debounce the request and drop a response that arrives after a newer query. Between keystrokes the highlight follows the command's `id`, so it stays put while results arrive and are replaced around it.

**Keyboard:** the focus stays in the search field. ↑ and ↓ move the highlight, wrapping and skipping disabled commands, Enter runs it, and Escape closes. Running a command calls its own `onClick`, then `onAction`, then `onClose`, the same order `Menu` uses. The number of matches is announced to screen readers as it changes.

On a small touch screen it can present as a bottom sheet like every `Modal`: pass `presentation={Presentation.Auto}`, or set it app-wide through `ConfigProvider`.

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `items` | `MenuEntry[]` | The commands. Headings group them; separators split groups (required) |
| `isOpen` | `boolean` | Whether the palette is open (required) |
| `onClose` | `() => void` | Called on Escape, a backdrop click, and after a command runs (required) |
| `onAction` | `(id: string) => void` | Called with the id of the command that ran, after its own `onClick` |
| `onQueryChange` | `(query: string) => void` | Called on every keystroke, and with `''` on open |
| `filter` | `boolean` | Filter `items` by the query (default `true`). `false` shows them as given |
| `isLoading` | `boolean` | Shows the `loading` label instead of `empty` while nothing matches |
| `presentation` | `Presentation` | Centred dialog (default), bottom sheet, or a sheet only on a small touch screen |

A command is a `MenuAction`: `id`, `label`, `icon`, `shortcut`, `keywords`, `disabled`, `danger`, `href` and `onClick`. A command with an `href` goes there after running, through `ConfigProvider`'s `navigate` when it is set. See [Menu.md](Menu.md).

**Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ CommandPalette: { presentation: Presentation.Auto } }}`. See [ConfigProvider.md](ConfigProvider.md).

**Slots:** `root` `search` `input` `list` `item` `itemIcon` `itemLabel` `itemShortcut` `heading` `separator` `empty`

**Labels:** `label` `placeholder` `empty` `loading` `results(count)`: the text this component renders of its own. Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; they merge per key. Type: `CommandPaletteLabels`.
