A ⌘K palette of commands that filters as you type, built on Base UI Dialog and Autocomplete and the Eased overlay core.
When to use
- One place to run any action or jump anywhere in an app: create an issue, go to a page, switch theme, open a teammate.
- Power users who live on the keyboard: ⌘K (Ctrl K off Apple) toggles it from anywhere, even from inside a text field, and Enter runs the first match.
- A long list of actions that would overflow a Dropdown Menu, or that people find by name rather than by position.
When not to use
- Picking a value for a form field: use Combobox or Select, which keep the choice.
- Searching content inside the page (documents, rows): use Search Field and filter in place.
- A handful of actions on one object: a Dropdown Menu or Context Menu sits next to the thing they act on.
Installation
npx shadcn add https://eased.dev/r/command-palette.jsonThe command adds Command Palette, everything it builds on and Eased’s tokens to your project.
Usage
import { CommandPalette, CommandPaletteTrigger, type CommandGroup } from "@/components/eased/command-palette/command-palette";const groups: CommandGroup[] = [ { label: "Suggestions", items: [ { id: "create", label: "Create issue", shortcut: ["C"], keywords: ["new"], onSelect: () => createIssue() }, { id: "docs", label: "Search docs", description: "Help", onSelect: () => openDocs() }, ], }, { label: "Navigation", items: [ { id: "inbox", label: "Go to Inbox", shortcut: ["G", "I"], onSelect: () => router.push("/inbox") }, { id: "settings", label: "Settings", onSelect: () => router.push("/settings") }, ], },];export function Header() { return ( <CommandPalette groups={groups}> <CommandPaletteTrigger className="w-56" /> </CommandPalette> );}Each command's onSelect runs, then the palette closes and focus goes back to whatever had it.
Examples
Opening it from elsewhere
useCommandPalette() holds the open state so a menu item or a button far from the trigger can open it.
const palette = useCommandPalette();<CommandPalette {...palette.props} groups={groups} /><Button onClick={palette.show}>Open commands</Button>A flat list and a different hotkey
<CommandPalette items={commands} hotkey="mod+shift+p" placeholder="Run a command…" footer={false} />More than one palette on a page
Only the first palette mounted answers a given hotkey. To give it to whichever is visible, pass hotkey={visible ? "mod+k" : false} (the demo does this with an IntersectionObserver).
API reference
CommandPalette
| Prop | Type | Default | Description |
|---|---|---|---|
groups | CommandGroup[] | Commands in labelled groups ({ label, items }). Groups with no matches hide while filtering; a group with an empty label is set off by a rule. | |
items | CommandItem[] | A flat list instead of groups. Ignored when groups is set. | |
open | boolean | Controlled open state. | |
defaultOpen | boolean | false | Uncontrolled starting state. |
onOpenChange | (open: boolean) => void | Called on every open and close: trigger, hotkey, Esc, outside click, a command. | |
hotkey | string | false | "mod+k" | Toggles the palette. mod is ⌘ on Apple platforms and Ctrl elsewhere; also ctrl, meta, shift, alt. Chords work inside text fields; bare keys ("/") don't. |
placeholder | string | "Search or jump to…" | The input's placeholder. |
inputLabel | string | "Search commands" | The input's accessible name. |
label | string | "Command palette" | The dialog's accessible name. |
emptyText | ReactNode | (query: string) => ReactNode | "No results" | Shown when nothing matches. |
footer | ReactNode | false | ↑↓ Navigate · ↵ Open · Esc Close | The bar under the list. |
loop | boolean | true | Arrow keys wrap from the last command through the input to the first. |
container | HTMLElement | null | document.body | Portal target. |
className | string | "" | Appended to the popup's classes (width is w-[36rem]). |
children | ReactNode | Triggers, usually CommandPaletteTrigger. |
CommandItem
| Field | Type | Description |
|---|---|---|
id | string | Unique within the palette. |
label | string | What the command says. Matched, with the matching run emphasised. |
icon | ReactNode | 16px icon or a 20px Avatar, in the leading track. |
description | ReactNode | Muted text in the trailing column. Matched when it is a string. |
shortcut | string[] | Key caps in the trailing column, e.g. ["G", "I"]. Display only. |
keywords | string[] | Extra words that find the command. Not shown. |
onSelect | () => void | Runs on click, tap or Enter; the palette then closes. |
disabled | boolean | Shown but can't be highlighted or run. |
Every word of the query must appear in the label, keywords, description or group label, compared with Base UI's locale-aware useFilter.
CommandPaletteTrigger
A button styled as a search field: magnifier, a muted label (children, default "Search…") and the hotkey's caps (hotkey, default "mod+k", false to hide). Base UI Dialog.Trigger props pass through. It presses by colour, never by scale.
useCommandPalette
useCommandPalette(defaultOpen?) returns { open, setOpen, show, hide, toggle, props }; spread props onto CommandPalette.
Keyboard
| Key | Action |
|---|---|
⌘K / Ctrl K | Opens the palette with no animation, or closes it. Works from inside text fields. |
| Typing | Filters the list; the first match is always highlighted. |
↓ / ↑ | Moves the highlight; past the last command it wraps through the input (loop). |
Enter | Runs the highlighted command and closes. |
Esc | Closes. Focus returns to where it was before the palette opened. |
Tab | Stays inside the palette (focus is trapped). |
Accessibility
- A modal
dialognamed bylabel; inside it a combobox input (named byinputLabel, described by a hidden hint on how to move, run and close) controlling alistboxof options in labelled groups. - The highlight is
aria-activedescendant, so focus stays in the input while the arrows move; the highlighted row scrolls into view. - "No results" is announced politely from a region that stays mounted. The default footer's caps are hidden from screen readers; the input's description says the same.
- The input advertises the hotkey with
aria-keyshortcuts. A visually hidden Close button is reachable by screen readers. - Focus moves to the input on open (on touch too, so the keyboard comes up) and returns to the trigger, or to the element that had focus when the hotkey was pressed.
- Rows are 40px, 44px under 640px. The list's height uses
dvhso it stays above the on-screen keyboard.
Motion
Opened by clicking its trigger, the palette rises 8px from 0.96 on the smooth spring while it fades in over 200ms, over a scrim that fades and blurs in 200ms. Opened from the hotkey it appears at once with no enter (rule 84: a keyboard-driven, high-frequency action), and the scrim still fades. Either way it leaves 4px at 0.98 in 160ms on the standard curve, never overshooting. Open and close are CSS transitions on Base UI's starting and ending styles, so a quick ⌘K ⌘K reverses mid-flight. Filtering and highlighting don't animate; the list's edges feather only where there is more to scroll. Under reduced motion it fades in 100ms and nothing moves.
Notes for AI
Pick CommandPalette for a global "run anything" surface; pass commands as data in groups, each with an onSelect, and put CommandPaletteTrigger inside it as a child. Use keywords for synonyms rather than adding duplicate commands; use useCommandPalette() when something other than the trigger or hotkey must open it.
Don't wrap it in another Dialog or add your own ⌘K listener: it owns the hotkey (one palette per hotkey per page), focus trapping, return focus, filtering, highlight and Esc. Don't close it from onSelect; it closes itself. It is not a value picker: use Combobox when the choice must stay in a form.
Related
- Dialog: the same surface and motion for any modal content.
- Search Field: a search input that filters content in place, with its own ⌘K focus hint.
- Kbd: the key caps in the footer and trigger.