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';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
| Prop | Type | |
|---|---|---|
| className | string | Added to the panel’s own classes, so yours wins |
| style | CSSProperties | Merged 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.
| Prop | Type | Default | |
|---|---|---|---|
| items | CommandItem[] | [] | |
| onSelect | (item: CommandItem) => void | — | For any command without one of its own |
| children | ReactNode | — | A trigger. Most palettes have none |
| open | boolean | — | Controlled |
| defaultOpen | boolean | false | |
| onOpenChange | (open: boolean) => void | — | |
| shortcut | string | null | 'k' | With ⌘ or Ctrl. null registers nothing |
| filter | (item, query) => boolean | every word, anywhere | |
| placeholder | string | 'Type a command…' | Also the accessible name |
| empty | ReactNode | 'No commands' | |
| footer | ReactNode | — | A row along the bottom |
| width | number | 560 | |
| maxHeight | number | 340 | Past this the list scrolls |
| radius | number | 14 | |
| offsetTop | number | string | '12vh' | How far down the window it sits |
| container | Element | null | document.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 K | Opens it, unless shortcut is null |
| ↑ ↓ | Walks the results, skipping disabled ones |
| Enter | Runs the active one |
| Esc | Closes, and focus returns where it was |