crispmeasured from Attio

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 crisp
import "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

PropTypeDefaultDescription
openbooleanWhether the palette is shown. Required — Command is fully controlled.
onOpenChange(open: boolean) => voidCalled when the dialog wants to close — Escape, backdrop click, or a selection.
itemsCommandItem[]defaultCommandItemsThe list of commands shown and filtered against.
placeholderstring"Search actions and records…"Placeholder text for the search input.

CommandItem

FieldTypeDescription
idstringUnique, stable identifier.
labelstringVisible text, also matched against the query.
iconReact.ComponentTypeLeading 16px icon component.
keywordsstring[]Extra terms that match this item without being shown.
shortcutstring[]Trailing shortcut hint, rendered as one Kbd per key (e.g. ["G", "H"]).
onSelect() => voidCalled when the item is chosen; the palette then closes.

Behavior

  • Filter — typing narrows items to those whose label or keywords match 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's onSelect and 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".

KeyAction
⌘K / Ctrl+KOpens the palette (when the host wires up the listener, as Sidebar does).
/ Moves the highlighted item.
EnterRuns the highlighted item and closes the palette.
EscapeCloses the palette.
Tab / Shift+TabCycles focus within the dialog only (focus trap).

On this page