# CommandPalette

> A ⌘K / Ctrl+K searchable command menu in a native dialog.

URL: https://apogee.avaui.dev/docs/command-palette
Last updated: 2026-10-21

A ⌘K / Ctrl+K searchable command menu in a native dialog.

Last updated October 21, 2026

Uses JavaScript

Opens on ⌘K / Ctrl+K (or a configurable `shortcut`), or from any element with `data-command-open="<id>"`. Typing filters commands by label and keywords; arrow keys move the highlight, Enter activates it — navigating to `href`, or dispatching an `apogee:command` event on `document`. Commands with a `shortcut` run from anywhere on the page, and key caps read Ctrl instead of ⌘ outside macOS.

**Without JS:** There is no fallback — the dialog and shortcut don't run at all. Always pair a trigger with a real, independently reachable link or menu.

```astro
<button type="button" data-command-open="apogee-command" class="rounded-md border border-neutral px-3 py-2 text-sm">Open (⌘K)</button>
<CommandPalette
commands={[
  { label: 'Create project', group: 'Actions', keywords: 'new add' },
  { label: 'Settings', href: '/settings', group: 'Navigate' },
  { label: 'Documentation', href: '/docs', group: 'Navigate' },
]}
/>
```

## Add

```sh
npx @ava-ui/apogee add command-palette
```

No Apogee dependencies. Uses `@lucide/astro` internally (`Search`) for the input icon — no icon import needed in your usage.

## Usage

```astro
---
import CommandPalette from '@/components/apogee/CommandPalette/CommandPalette.astro'
---
<button type="button" data-command-open="apogee-command">Search…</button>
<CommandPalette
  commands={[
    { label: 'Create project', group: 'Actions' },
    { label: 'Settings', href: '/settings', group: 'Navigate' },
  ]}
/>
```

## Anatomy

-   **Dialog** — native `<dialog>`; modal, backdrop, and `closedby="any"` come free.
-   **Search input** — `role="combobox"`, filters commands as you type.
-   **Groups** — commands grouped by `group`, in first-seen order; ungrouped commands render without a heading.
-   **Options** — `role="option"` buttons, one per command; an empty-state message shows when nothing matches.

## API

`@composable` — exposed to Content Editors in AVA. No passthrough attrs — the dialog doesn’t spread `...rest`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | `'apogee-command'` | Dialog id; also the target for `data-command-open="<id>"` triggers. |
| `commands` | `{ label; href?; group?; keywords?; id?; icon?; avatar?; description?; shortcut?; stacked? }[]` | — | The command list. Required. `icon` is a Lucide component; `avatar` is `{ src?, name? }`; `description` shows after the label, or under it with `stacked`; `shortcut` (e.g. `['alt', 'shift', 'n']`) renders key caps and runs the command from anywhere on the page. |
| `placeholder` | `string` | `'Type a command or search…'` | Search input placeholder. |
| `shortcut` | `string` | `'k'` | Key pressed with ⌘ / Ctrl to open. |
| `src` | `string` | — | URL of a JSON array of more commands (`{ label, href?, group?, keywords?, description? }`), fetched the first time the palette opens and added after `commands`. |
| `class` | `string` | — | Extra classes on the `<dialog>`. |

Slots: `empty` replaces “No results.” (an EmptyState fits); `footer` is a row of key hints under the list, shown from `md`.

## Rich commands

Icons, avatars, descriptions and shortcuts. Try typing “resume” (accents are ignored) or press ⌘⇧P / Ctrl+Shift+P anywhere on this page.

```astro
<CommandPalette id="rich-command" commands={[
{ label: 'New project', id: 'new-project', group: 'Actions', icon: FolderPlus, shortcut: ['alt', 'shift', 'n'] },
{ label: 'Résumé', group: 'Files', icon: FileText, description: 'Updated yesterday' },
{ label: 'Ana Lima', group: 'People', avatar: { name: 'Ana Lima' }, description: 'Product designer', stacked: true },
]}>
<EmptyState slot="empty" variant="plain" size="small" title="Nothing matches"><SearchX slot="icon" /></EmptyState>
<Fragment slot="footer"><span><Kbd>↑</Kbd> <Kbd>↓</Kbd> to move</span><span><Kbd>↵</Kbd> to select</span><span><Kbd>esc</Kbd> to close</span></Fragment>
</CommandPalette>
```

## Long lists

A palette with hundreds of commands makes every page heavy. Pass `src` instead: the palette fetches the JSON the first time it opens and builds those commands then. This page’s palette loads all of these docs from `/search.json`. Commands from `src` take a label, link, group, keywords and description; icons, avatars and shortcuts stay with `commands`.

```astro
<CommandPalette id="docs-command" commands={[]} src="/search.json" />
```

## States

-   **Closed** — default; the dialog isn’t rendered visibly until opened.
-   **Open** — backdrop + dialog fade in (`@starting-style`).
-   **Highlighted option** — the active result (`aria-selected`), moved with arrow keys or reset to the first result on every filter.
-   **Empty** — “No results.” (or the `empty` slot) shows when nothing matches the query; empty groups hide their heading.

## Examples

Grouped commands with keywords (aliases the label doesn’t contain):

```astro
<CommandPalette
  commands={[
    { label: 'Create project', group: 'Actions', keywords: 'new add' },
    { label: 'Invite teammate', group: 'Actions', keywords: 'member user' },
    { label: 'Settings', href: '/settings', group: 'Navigate' },
  ]}
/>
```

A command with no `href` dispatches `apogee:command` for the app to handle:

```astro
---
// listen on the client:
// document.addEventListener('apogee:command', (e) => { ... e.detail.id })
---
<CommandPalette
  id="app-command"
  commands={[{ label: 'Toggle theme', id: 'toggle-theme' }]}
/>
```

Custom shortcut key:

```astro
<CommandPalette shortcut="/" commands={[{ label: 'Search docs', href: '/docs' }]} />
```

## Accessibility

| Key | Action |
| --- | --- |
| `⌘`/`Ctrl` + `shortcut` | Opens the palette (default `K`). |
| `↓` `↑` | Move the highlight; stops at the first and last result. |
| `Enter` | Runs the highlighted command. |
| `Esc` | Closes the palette. |
| Command `shortcut` | Runs that command from anywhere on the page, except while typing in a field. |

-   Native `<dialog>` gives a focus trap, `Esc`\-to-close, and `closedby="any"` backdrop-click-to-close for free.
-   `role="combobox"` input wired with `aria-expanded`, `aria-controls`, `aria-autocomplete="list"`; results are `role="option"` with `aria-selected`.
-   **No JS:** there is no fallback — the dialog, shortcut, and filtering are entirely JS-driven with no `no-js:` variant. Always give commands a real, independently reachable trigger (a visible link or menu item) alongside the palette.

## Tokens

| Token | Used for |
| --- | --- |
| `--border-neutral` | Dialog border, header divider |
| `--background-page` | Dialog background |
| `--foreground-neutral` | Option text |
| `--foreground-neutral-faded` | Search icon, placeholder, empty state |
| `--foreground-neutral` | Group heading |
| `--border-neutral` | Rule between groups |
| `--background-neutral-faded-highlighted` | Option hover / selected background |
| `--duration-fast` / `--ease-standard` | Open/close transition |

## Do / Don’t

-   **Do** keep `commands` reasonably sized — filtering is a client-side substring match against label + keywords, so very large lists get sluggish.
-   **Do** use `keywords` for synonyms a user might type instead of the exact label.
-   **Don’t** mount two palettes with the same `shortcut` on one page — the first keydown listener wired wins.
-   **Don’t** treat the palette as the only way to reach a command — always pair it with a visible trigger.

## Content

-   Command labels are short: verb-led when they act (“Create project”), noun-led when they navigate (“Settings”).
-   Group names are short category nouns, sentence case.
-   `placeholder` states what can be typed; the default (“Type a command or search…”) works for most cases.

## Related

[

DialogA native <dialog> opened and closed with invoker commands — modal, edge panel, or fullscreen.

](https://apogee.avaui.dev/docs/dialog)[

ComboboxA text input with a styled, filterable dropdown of suggestions, anchored under the input in the top layer.

](https://apogee.avaui.dev/docs/combobox)[

KbdA single keyboard key or shortcut, styled like a physical key.

](https://apogee.avaui.dev/docs/kbd)
