Overlay

Command palette

Every command in the app, one keystroke away. It is the only component here with no entrance and no exit, and that is the motion standard's own rule rather than an exception to it.

Usage

import { CommandPalette } from '@carabine/ui/command';
<CommandPalette
  items={commands}
  onSelect={(item) => go(item.id)}
  placeholder="Type a command…"
  footer={<span>↑↓ to move · ↵ to run · esc to close</span>}
/>

Most palettes have no trigger at all. ⌘K is the interface; a button that opens it is a convenience for people who have not learned the chord yet.

Why it does not move

Every other overlay here animates, and this one does not — because the standard says entrances are for things that arrive, and a palette does not arrive. It is summoned by a chord, and the person who pressed it is already typing before the first frame would have finished. An entrance here is 150ms of the interface not being ready yet.

The rule generalises: the faster a thing is invoked, the less it should move. A dialog opens once and can afford 200ms. A palette opens forty times a day.

The active row is still a sliding bar, because that follows a keystroke rather than an opening. It is measured from the same box as the rows — offsetTop is border-box to border-box, so the bar sits at top: 0 inside the padding rather than being offset by it, which is a bezel counted twice and reads as every row nudged upward inside its own highlight.

Examples

Filtering

The default filter matches every word of the query anywhere in the label, the description or the keywords — so “dep prod” finds Deploy to production. There is no ranking and no fuzzy scoring: the order you passed is the order shown, because a palette that reorders itself as you type is a palette where the row under your finger is not the one you were about to press.

<CommandPalette
  items={commands}
  filter={(item, query) => item.label.toLowerCase().startsWith(query.toLowerCase())}
/>

Customization

<CommandPalette
  shortcut="p"        // with ⌘ or Ctrl. null registers nothing
  width={640}
  maxHeight={420}     // past this the list scrolls
  radius={14}
  offsetTop="18vh"    // how far down the window it sits
  empty="Nothing by that name"
  theme="light"
/>

Styling reference

Two bezels, the toast's: a 6px shell around a core, and the inner radius is the outer minus the shell. The rows sit on the core and the active bar spans it edge to edge — left: BEZEL, right: BEZEL — because a highlight that stops short of the frame looks like a row that failed to fill.

The scrollbar lives in command.css: thin, no track, transparent until the pointer is on the list.

API reference

PropType
classNamestringAdded to the panel’s own classes, so yours wins
styleCSSPropertiesMerged after the component’s own inline styles, so yours wins

Both land on the panel. The trigger is already your element — you style it where you write it — and the panel is the part that portals away from your markup and is otherwise out of reach. State is legible from CSS as well — data-state, data-disabled, data-side — so a rule can answer it without knowing a single class name of ours. See Theming.

PropTypeDefault
itemsCommandItem[][]
onSelect(item: CommandItem) => voidFor any command without one of its own
childrenReactNodeA trigger. Most palettes have none
openbooleanControlled
defaultOpenbooleanfalse
onOpenChange(open: boolean) => void
shortcutstring | null'k'With ⌘ or Ctrl. null registers nothing
filter(item, query) => booleanevery word, anywhere
placeholderstring'Type a command…'Also the accessible name
emptyReactNode'No commands'
footerReactNodeA row along the bottom
widthnumber560
maxHeightnumber340Past this the list scrolls
radiusnumber14
offsetTopnumber | string'12vh'How far down the window it sits
containerElement | nulldocument.body
theme'dark' | 'light''dark'

CommandItem carries id, label, description, icon, shortcut, keywords, group, disabled and its own onSelect, which wins over the shared handler.

Keyboard

Key
⌘K / Ctrl KOpens it, unless shortcut is null
↑ ↓Walks the results, skipping disabled ones
EnterRuns the active one
EscCloses, and focus returns where it was