Command
The ⌘K command palette — a filterable, keyboard-navigable list of actions and records.
Command is one of crisp's Product-layer building blocks — Sidebar mounts it once and opens it from its search field or a global ⌘K / Ctrl+K listener. It composes a Zag dialog machine (modal, focus trap, scroll lock) with a Zag combobox machine (filtering, keyboard navigation) so correctness comes from the machines, not hand-rolled event handling.
Measured from Attio — 726px wide, r16 dialog, 36px / r12 items.
Preview
Click the button below, or press ⌘K / Ctrl+K anywhere on this page — the same shortcut Sidebar wires up.
Installation
bun add crispimport "crisp/styles.css";
import { Command } from "crisp";Usage
Command is controlled — the caller owns open state and decides what opens it (a button, a keyboard shortcut, or both).
"use client";
import * as React from "react";
import { Command } from "crisp";
export function Example() {
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
event.preventDefault();
setOpen(true);
}
}
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
return (
<>
<button onClick={() => setOpen(true)}>Open command palette</button>
<Command open={open} onOpenChange={setOpen} />
</>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Whether the palette is shown. Required — Command is fully controlled. |
onOpenChange | (open: boolean) => void | — | Called when the dialog wants to close — Escape, backdrop click, or a selection. |
items | CommandItem[] | defaultCommandItems | The list of commands shown and filtered against. |
placeholder | string | "Search actions and records…" | Placeholder text for the search input. |
CommandItem
| Field | Type | Description |
|---|---|---|
id | string | Unique, stable identifier. |
label | string | Visible text, also matched against the query. |
icon | React.ComponentType | Leading 16px icon component. |
keywords | string[] | Extra terms that match this item without being shown. |
shortcut | string[] | Trailing shortcut hint, rendered as one Kbd per key (e.g. ["G", "H"]). |
onSelect | () => void | Called when the item is chosen; the palette then closes. |
Behavior
- Filter — typing narrows
itemsto those whoselabelorkeywordsmatch the query, case-insensitively. ↑/↓— moves the highlighted item; the list wraps and auto-highlights the first match as you type.Enter— runs the highlighted item'sonSelectand closes the palette.Escape/ backdrop click — closes the palette without selecting anything.- Reopening always starts from a cleared query and a fresh highlight — it never resumes a stale filter from the previous time it was open.
Examples
Controlled from a button
The example in Preview above — a Button sets open to true; Command calls onOpenChange(false) on close.
<Button intent="primary" onClick={() => setOpen(true)}>
Open command palette
</Button>
<Command open={open} onOpenChange={setOpen} />Custom items
import { House, Settings } from "lucide-react";
const items = [
{ id: "home", label: "Go to Home", icon: House, shortcut: ["G", "H"] },
{ id: "settings", label: "Settings", icon: Settings, keywords: ["preferences"] },
];
<Command open={open} onOpenChange={setOpen} items={items} />;Accessibility
The dialog is rendered through a Portal and traps focus while open — Tab cycles only through its own controls, and focus returns to the trigger on close. The search input receives initial focus automatically. The option list uses role="option" / aria-selected (from the combobox machine), so the highlighted item is announced to screen readers as the keyboard moves. The whole dialog carries aria-label="Command palette".
| Key | Action |
|---|---|
⌘K / Ctrl+K | Opens the palette (when the host wires up the listener, as Sidebar does). |
↑ / ↓ | Moves the highlighted item. |
Enter | Runs the highlighted item and closes the palette. |
Escape | Closes the palette. |
Tab / Shift+Tab | Cycles focus within the dialog only (focus trap). |