Cronwerk UI

Command palette recipes

Three copy-paste CommandPalette configurations — the landing actions palette, the docs static-index search, and the volksdroid async catalog jump.

stable

Overview

The CommandPalette is one surface with three consumers. Each recipe below is the whole configuration — copy it, swap the data, and wire the open state to your own trigger or useShortcut. The palette shares the Dialog overlay contract (portal, focus trap, Escape/backdrop close) and the fuzzy matcher; what changes per consumer is the source and the shortcut.

import { CommandPalette, useShortcut } from "@cronwerk/ui";
ConsumerSourceShortcutNotes
Landing actionsstatic items, grouped⌘⇧Psmall curated action set
Docs searchstatic index (searchText + recency)⌘Krecency seeds the empty query
Volksdroid catalogasync source⌘Kdebounced + virtualized (2,000+)

Opening with a shortcut

Every recipe pairs the controlled open state with useShortcut. The default is mod+k (⌘K on macOS, Ctrl+K elsewhere); the landing uses mod+shift+p so it never collides with the browser’s find. Shortcuts are suppressed while an editable field has focus, and a duplicate binding warns in development.

const [open, setOpen] = useState(false);
useShortcut("mod+shift+p", () => setOpen(true)); // ⌘⇧P / Ctrl+Shift+P

Recipe 1 — Landing actions palette

A small, curated command set grouped by intent. Static items are fuzzy-matched and ranked client-side; groups fixes the section order.

Landing actions source
const groups = [
{ id: "start", label: "Get started" },
{ id: "product", label: "Product" },
{ id: "company", label: "Company" },
];
const actions = [
{ id: "start-free", label: "Start free", group: "start", hint: "↵" },
{ id: "pricing", label: "See pricing", group: "start", keywords: ["plans"] },
{ id: "workflows", label: "Workflows", group: "product" },
// …
];

function Palette() {
const [open, setOpen] = useState(false);
useShortcut("mod+shift+p", () => setOpen(true));
return (
  <CommandPalette
    open={open}
    onOpenChange={setOpen}
    items={actions}
    groups={groups}
    ariaLabel="Site actions"
    placeholder="Jump to anything…"
  />
);
}

Recipe 3 — Volksdroid catalog jump (async)

The marketplace has thousands of listings, so the palette streams from an async source and virtualizes the list. The source is debounced and cancellable — return a promise that honors the AbortSignal, and the palette handles the loading, error, and empty states.

Async catalog source source
const source: CommandSource = (query, signal) =>
fetch(`/api/catalog?q=${encodeURIComponent(query)}`, { signal })
  .then((r) => r.json());

<CommandPalette
open={open}
onOpenChange={setOpen}
source={source}
debounceMs={180}
itemHeight={52}
ariaLabel="Catalog jump"
loadingState="Searching the catalog…"
placeholder="Search 2,000 droids…"
/>;

Usage

Do

Keep one palette instance per app, open it from a trigger and a shortcut, and feed it whichever source fits — static for curated actions, async for large or remote sets.

Don't

Don’t bind two palettes to the same shortcut, and don’t pass a giant static items array when the data is remote — reach for a source so queries are debounced and cancelled.

Accessibility

The palette is an APG combobox with a listbox popup: focus stays in the input while aria-activedescendant moves a virtual cursor over the options. Arrow keys move the active row, Home/End jump to the ends, Enter selects, and Escape closes (restoring focus to the trigger via the Dialog focus trap). Group headings are exposed through role="group" + aria-labelledby; the empty state is announced without stealing the cursor.