Command
An agent-native ⌘K command palette — a searchable, keyboard-first list that navigates nested sub-command pages, handles async loading / error / empty states, ranks recents, binds per-item shortcuts that actually fire, and opens a per-row action panel on → / ⌘.. CommandDialog is the canonical host (⌘/Ctrl + optional `/` open-shortcut, controllable open, imperative apiRef).
@garn/ui/commandDefault
Inline command list with grouped, filterable items and display-only shortcut hints.
Overview
Basic
Inline command list with grouped, filterable items and display-only shortcut hints.
Palette
The canonical ⌘K palette: a modal CommandDialog that binds its own open shortcut, floats recents up (useRecentCommands), groups sections, binds per-item shortcuts that fire, and shows a Kbd footer.
Async
Async Search
Search-as-you-type against an async source via useCommandSearch (debounce + cancel); CommandLoading while fetching, CommandError with Retry on failure (type 'error'), shouldFilter off.
States
Loading State
A static snapshot of the loading state — CommandLoading shows and CommandEmpty is suppressed (the loading↔empty guard).
Error State
A static snapshot of the async-failure state — CommandError (role=alert) with a Retry button.
Empty State
A static snapshot of the no-match state — a query that matches nothing renders 'No results for ‘…’'.
Actions
Actions
Per-item action panel — each row declares CommandItemActions; ↵ runs the primary, → / ⌘. opens the row's actions (each with a shortcut), Esc closes just the panel.
Recipes
Faceted Filter
A faceted multi-select filter recipe — Button + Popover + Command + Counter + Badge ('Status ⌄ 3 selected').
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add command- Registry
command- Source
- command.tsx
- Deps
lucide-react- Registry deps
utilscmdkdialogpopoverbuttonspinneruse-announceuse-controllable-stateuse-debounced-valueuse-event-listeneruse-local-storageuse-merged-refuse-prefers-reduced-motion
Import
import { Command } from "@garn/ui/command";Props
Generated from the component's TypeScript types.
apiRef- Description
- Imperative handle — drive the palette programmatically (agent-native).
- Type
React.Ref<CommandApi>
defaultSearch- Description
- No description.
- Type
string
loading- Description
- In-flight async results. Auto-shows `CommandLoading`, hides `CommandEmpty` (so they can't both render — cmdk #269), and turns filtering off.
- Type
boolean
onSearchChange- Description
- No description.
- Type
(value: string) => void
prefixes- Description
- Leading-token → page routing (VS Code Quick Open): typing `>` at root jumps to a scope page and strips the token. Keyed by token, e.g. `{ ">": { page: "commands", label: "Commands" } }`.
- Type
CommandPrefixMap
search- Description
- Controlled search text. Uncontrolled by default via `defaultSearch`.
- Type
string
Plus 289 inherited native HTML attributes.
CommandDialog
"data-slot"- Description
- `data-slot` for the content surface (AppShellCommand overrides it).
- Type
string
apiRef- Description
- Imperative handle — open/close plus the inner Command's content ops.
- Type
React.Ref<CommandDialogApi>
className- Description
- Class for the inner `Command`.
- Type
string
contentClassName- Description
- Class for the `DialogContent` surface.
- Type
string
description- Description
- No description.
- Type
React.ReactNode
openOnSlash- Description
- Also open on a bare `/` when focus isn't in a text field.
- Type
boolean
shortcut- Description
- Key for the ⌘/Ctrl open-shortcut; `null`/omitted disables.
- Type
string
title- Description
- No description.
- Type
React.ReactNode
Plus 296 inherited native HTML attributes.
CommandError
onRetry- Description
- Renders a Retry button when provided.
- Type
() => void
Plus 280 inherited native HTML attributes.
CommandItem
shortcut- Description
- A modifier-gated combo (`"mod+p"`, `"mod+shift+c"`) that both renders as a trailing shortcut hint AND fires this item's `onSelect` while the palette is focused. `mod` = ⌘ on mac, Ctrl elsewhere.
- Type
string
Plus 285 inherited native HTML attributes.
CommandItemActions
hint- Description
- Inline affordance on the row. Defaults to a subtle `⌘.` hint; pass `null` to hide it.
- Type
React.ReactNode
Plus 280 inherited native HTML attributes.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="command"][data-slot="command-dialog"][data-slot="command-input-wrapper"][data-slot="command-back"][data-slot="command-action-panel"]| Property | Token | Tier |
|---|---|---|
inputHeight | --garn-control-h-lg | semantic · air |
itemMinHeight | --garn-control-h-md | semantic · air |
scrollFade | --garn-space-48 | semantic |
pageMotionEasing | --garn-ease-standard | semantic |
Overview
Command lets users find and run an action — or jump to a thing — by typing: a ⌘K palette, a searchable action menu, a quick switcher. It's cmdk's combobox+listbox engine wearing the garn layer, elevated to do four things a thin wrapper can't. It NAVIGATES: push nested sub-command pages (pick a project → pick an action) with a back-pill and Backspace-to-pop, or jump straight in with a leading-token prefix. It WAITS: CommandLoading / CommandError / a refined CommandEmpty are first-class async states, not a single 'no results'. It REMEMBERS: useRecentCommands ranks by recency + frequency and biases the filter so power users' commands float up. And it ACTS: an item's shortcut both shows AND fires, and CommandItemActions gives each row a secondary-action panel on → / ⌘.. CommandDialog is the one canonical host — it owns the open-shortcut, controllable open state, and an imperative apiRef (agent-native), and AppShellCommand is a thin pass-through over it. Four headless hooks carry the behavior: useCommandShortcut (bind the open key), useCommandPages (read/drive the stack), useRecentCommands (ranking + injectable store), useCommandSearch (debounced async with request cancellation).
Guidelines
Reach for the canonical host. Use CommandDialog with a shortcut (and maybe openOnSlash) for the global ⌘K palette; the bare Command is for an inline palette (a filter popover, a picker). Don't hand-roll a second ⌘K listener — that's exactly the duplication CommandDialog folds away.
Model async through the flags, not by hand. useCommandSearch owns the debounce, the loading/error/empty machine, and cancels stale requests; wire query/setQuery to CommandInput, loading to Command, and render CommandLoading / CommandError / CommandEmpty off the flags. Pass shouldFilter={false} when the server already returned the matches, so cmdk doesn't re-filter them.
Rank with recents. Call record(value) in an item's onSelect and pass useRecentCommands().filter to <Command filter={…}> to float recents up; render a 'Recent' CommandGroup from recent. The store is injectable (localStorage by default) so it can ride a consumer store.
Bind shortcuts, don't just show them. Put the combo on CommandItem's shortcut (it renders the hint AND fires the item) rather than a bare CommandShortcut (display-only). Every combo needs a non-shift modifier.
Keep it keyboard-driven and named. Type-to-filter, arrow to move, Enter to run, Esc to close; → / ⌘. for a row's action panel; Backspace-on-empty to go back. It's a combobox over a listbox — preserve that. Name the CommandDialog for its context via title/description.
Best practices
- Use CommandDialog with a
shortcutfor the global ⌘K palette (andopenOnSlashfor/); the bare Command for an inline palette. - Always include a CommandEmpty for the no-results state, and give non-text items a
valueso cmdk can match them. - Model async with
useCommandSearch+loading+shouldFilter={false}, and render CommandLoading / CommandError / CommandEmpty off the flags. - Float recents up with
useRecentCommands:record()in onSelect and pass itsfilterto Command. - Bind keys via CommandItem's
shortcut(renders AND fires) — not a bare CommandShortcut. - Declare per-row secondary actions with CommandItemActions + CommandAction, and pass the CommandAction children directly (not wrapped in a fragment / .map).
- Name CommandDialog via
title/description, and add a CommandFooter of Kbd hints for discoverability. - Drive the palette from an agent / toolbar via
apiRef(CommandApi / CommandDialogApi).
- Don't hand-roll a second ⌘K listener — CommandDialog is the one host (AppShellCommand is a pass-through over it).
- Don't rely on CommandShortcut to bind the key — it's display-only; use CommandItem's
shortcut, and always include a modifier. - Don't leave cmdk filtering on for async results — pass
shouldFilter={false}or the fetched matches get re-filtered. - Don't ship a palette with no empty state, and don't show 'no results' while loading (CommandEmpty is guarded, but only if you render it inside CommandList).
- Don't wrap CommandAction children in a fragment or a .map()-array — the shortcut scan + panel render read them one level deep.
Content guidelines
- Group commands under short headings and phrase each item as the action ('Create issue').
- Write a clear placeholder and a helpful empty state ('No results for ‘…’'); distinguish 'no query yet' (show recents) from 'query, no match'.
- Keep shortcut hints to the real binding, and keep the footer terse ('↵ select · → actions · esc close').
Accessibility
- Role
combobox- ARIA APG
- combobox
- Focus
- Focus stays in the input; ArrowUp/Down move the active option (aria-activedescendant). The back-pill and the per-row action panel both restore focus to the input on close/pop, so the tab stop never falls to <body>. Don't strip the focus ring.
Enforceable contracts
CommandDialog names the dialog via an sr-only title/description (default 'Command menu'); override `title`/`description` for context, or pass `null` and supply your own DialogTitle.
when always (CommandDialog)
The combo renders as a trailing hint AND fires the item's onSelect while the palette is focused; it must carry a non-shift modifier (mod / ctrl / alt) so it never collides with type-ahead.
when a CommandItem sets `shortcut`
The back affordance is a button with an accessible name ('Go back — {label}'); popping it (or Backspace on an empty input) returns focus to the input and announces the navigation politely.
when a sub-command page is pushed
The row exposes aria-haspopup; → / ⌘. opens a focus-scoped panel whose Esc closes only the panel and restores focus to the input (never the outer dialog).
when a row renders CommandItemActions
Result counts and page navigation announce via a polite live region and CommandError is role=alert; CommandEmpty is suppressed while `loading` so 'no results' never renders during a fetch.
when results filter or async state changes
Keyboard
| A–Z | Type in the input to filter the list. |
| ArrowUpArrowDown | Move between matching items. |
| Enter | Run the highlighted item (its onSelect). |
| Escape | Close the palette (CommandDialog) — or, when a row's action panel is open, close just the panel. |
| Backspace | On an empty input, pop the current sub-command page (or exit a prefix scope). |
| ArrowRight | When the caret is at the end of the query, open the highlighted row's action panel (if it has actions). |
| Mod. | Open the highlighted row's action panel. |
| ModK | Open / toggle the palette (CommandDialog with a `shortcut`). |
| / | Open the palette when focus isn't in a text field (CommandDialog `openOnSlash`). |
| Mod… | Fire a highlighted item's bound `shortcut` (e.g. ⌘P) without leaving the input. |
- • CommandInput is role=combobox controlling the role=listbox CommandList; items are role=option with aria-selected. Filtering is by item text/`value` — give non-text items a `value` so they're searchable.
- • CommandDialog carries an sr-only DialogTitle/DialogDescription (default 'Command menu') so Radix always has an accessible name; focus is trapped and returns to the opener on close.
- • On a nested page the input grows a back-pill — a real button named 'Go back — {label}'; popping it (button or Backspace-on-empty) restores focus to the input, and page navigation is announced via a polite live region.
- • A row with secondary actions (CommandItemActions) gets aria-haspopup + aria-keyshortcuts; → / ⌘. opens a focus-scoped Popover panel whose Esc closes ONLY the panel (not the outer dialog) and returns focus to the input.
- • Filtered result counts are announced politely ('N results' / 'No results'); the async-failure slot (CommandError) is role=alert (assertive). CommandEmpty is suppressed while `loading`.
- • Page-push motion, the edge-fade, and hover affordances are decorative — the fade is offset by a per-row scroll-margin so keyboard navigation never parks a row under it, and motion is gated by prefers-reduced-motion.
- Status
- Stable
- Version
v0.3.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.