Overlays & navigation
Command
Command is a searchable list of actions and destinations: the ⌘K palette that power users live in. It filters as you type (matching labels, word starts and extra keywords, ranked by quality), groups results under headings, shows icons, hints and shortcuts, and is driven entirely from the keyboard. Use it inline in a card or panel, or in CommandDialog as a modal palette.
const groups: CommandGroup[] = [ { heading: "Suggestions", items: [ { id: "new-invoice", label: "New invoice", icon: <FilePlus2 strokeWidth={1.75} />, shortcut: ["⌘", "N"], keywords: ["create", "bill"] }, { id: "new-client", label: "Add client", icon: <UserPlus strokeWidth={1.75} />, keywords: ["customer", "create"] }, { id: "expense", label: "Record expense", icon: <Receipt strokeWidth={1.75} />, keywords: ["receipt", "cost"] }, ], }, { heading: "Go to", items: [ { id: "dashboard", label: "Dashboard", icon: <LayoutDashboard strokeWidth={1.75} />, shortcut: ["G", "D"] }, { id: "invoices", label: "Invoices", icon: <FileText strokeWidth={1.75} />, shortcut: ["G", "I"] }, { id: "clients", label: "Clients", icon: <Users strokeWidth={1.75} />, shortcut: ["G", "C"] }, { id: "reports", label: "Reports", icon: <BarChart3 strokeWidth={1.75} />, hint: "Revenue, tax, aging" }, ], }, { heading: "Settings", items: [ { id: "profile", label: "Profile", icon: <User strokeWidth={1.75} /> }, { id: "billing", label: "Billing and plan", icon: <CreditCard strokeWidth={1.75} />, keywords: ["subscription", "payment"] }, { id: "appearance", label: "Appearance", icon: <Palette strokeWidth={1.75} />, keywords: ["theme", "dark mode"] }, { id: "settings", label: "Workspace settings", icon: <Settings strokeWidth={1.75} /> }, ], },]; export function InlineCommand() { const [picked, setPicked] = useState<string | null>(null); return ( <div className="mx-auto flex w-full max-w-xl flex-col gap-3"> <div className="overflow-hidden rounded-ui-xl border border-line shadow-pop"> <Command groups={groups} onSelect={(item) => setPicked(item.label)} className="max-h-96" /> </div> <p role="status" className="m-0 h-5 text-center text-[13px] text-muted"> {picked ? `Ran “${picked}”` : ""} </p> </div> );}Usage
import { Command, type CommandGroup } from "@/components/ui/command"; const groups: CommandGroup[] = [ { heading: "Go to", items: [ { id: "invoices", label: "Invoices", icon: <FileText strokeWidth={1.75} />, onSelect: () => router.push("/invoices") }, ], },]; <Command groups={groups} />Examples
Inline
Type to filter: “bill” finds New invoice through its keywords. Arrow keys move the highlight, Enter runs the item.
const groups: CommandGroup[] = [ { heading: "Suggestions", items: [ { id: "new-invoice", label: "New invoice", icon: <FilePlus2 strokeWidth={1.75} />, shortcut: ["⌘", "N"], keywords: ["create", "bill"] }, { id: "new-client", label: "Add client", icon: <UserPlus strokeWidth={1.75} />, keywords: ["customer", "create"] }, { id: "expense", label: "Record expense", icon: <Receipt strokeWidth={1.75} />, keywords: ["receipt", "cost"] }, ], }, { heading: "Go to", items: [ { id: "dashboard", label: "Dashboard", icon: <LayoutDashboard strokeWidth={1.75} />, shortcut: ["G", "D"] }, { id: "invoices", label: "Invoices", icon: <FileText strokeWidth={1.75} />, shortcut: ["G", "I"] }, { id: "clients", label: "Clients", icon: <Users strokeWidth={1.75} />, shortcut: ["G", "C"] }, { id: "reports", label: "Reports", icon: <BarChart3 strokeWidth={1.75} />, hint: "Revenue, tax, aging" }, ], }, { heading: "Settings", items: [ { id: "profile", label: "Profile", icon: <User strokeWidth={1.75} /> }, { id: "billing", label: "Billing and plan", icon: <CreditCard strokeWidth={1.75} />, keywords: ["subscription", "payment"] }, { id: "appearance", label: "Appearance", icon: <Palette strokeWidth={1.75} />, keywords: ["theme", "dark mode"] }, { id: "settings", label: "Workspace settings", icon: <Settings strokeWidth={1.75} /> }, ], },]; export function InlineCommand() { const [picked, setPicked] = useState<string | null>(null); return ( <div className="mx-auto flex w-full max-w-xl flex-col gap-3"> <div className="overflow-hidden rounded-ui-xl border border-line shadow-pop"> <Command groups={groups} onSelect={(item) => setPicked(item.label)} className="max-h-96" /> </div> <p role="status" className="m-0 h-5 text-center text-[13px] text-muted"> {picked ? `Ran “${picked}”` : ""} </p> </div> );}Command palette (⌘J)
CommandDialog opens the same list in a modal pinned near the top. Wire the shortcut yourself; this demo uses ⌘J (Ctrl+J) because the docs search already owns ⌘K. Choosing an item closes the palette.
export function CommandPalette() { const [open, setOpen] = useState(false); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key.toLowerCase() === "j" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen((o) => !o); } }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, []); return ( <div className="flex justify-center"> <button type="button" onClick={() => setOpen(true)} className="flex h-9 w-64 items-center gap-2 rounded-ui border border-line-strong bg-surface px-3 text-[13px] text-muted shadow-xs transition-colors hover:bg-raised" > <Search strokeWidth={1.75} className="size-4" /> <span className="flex-1 text-left">Search Ledgerly…</span> <Kbd>⌘</Kbd> <Kbd>J</Kbd> </button> <CommandDialog open={open} onClose={() => setOpen(false)} groups={groups} placeholder="Search or jump to…" /> </div> );}Empty state
Pass a string, a node, or a function of the query to empty. A “Create …” action turns a dead end into a next step.
<Command groups={clients} label="Find a client" placeholder="Find a client…" defaultQuery="Harbour Lane" footer={false} empty={(q) => ( <div className="flex flex-col items-center gap-3"> <span> No client called <span className="font-medium text-fg">“{q}”</span> yet. </span> <Button size="sm" variant="soft" iconLeft={<Plus strokeWidth={1.75} />}> Create “{q}” </Button> </div> )}/>Props
Command
groupsrequiredCommandGroup[]default: —{ heading, items }[] in display order. Empty groups are hidden while filtering.placeholderstringdefault: "Type a command or search…"Search field placeholder.labelstringdefault: "Command menu"Accessible name of the search field and the list.emptyReactNode | (query) => ReactNodedefault: "No results found."Shown when nothing matches.onSelect(item: CommandItem) => voiddefault: —Called after the item's own onSelect, e.g. to close a dialog or log usage.footerbooleandefault: trueKeyboard hints bar (hidden on phones).defaultQuerystringdefault: ""Initial search text.autoFocusbooleandefault: falseFocus the search field on mount (CommandDialog does this for you).
CommandItem
idrequiredstringdefault: —Unique within the command.labelrequiredstringdefault: —Visible text, also matched by the search.iconReactNodedefault: —Lucide icon before the label.hintstringdefault: —Quiet secondary text after the label (hidden on phones).shortcutstring[]default: —Keys shown on the right, e.g. ["G", "I"]. Display only.keywordsstring[]default: —Extra words that match: synonyms, ids, old names.disabledbooleandefault: —Shown dimmed and skipped by the keyboard.onSelect() => voiddefault: —Runs on Enter or click.
CommandDialog
open / onCloserequiredboolean / () => voiddefault: —Same as Dialog. The search resets every time it opens.…Command propsdefault: —Everything Command takes, except autoFocus.
Accessibility
- The search field is a combobox (aria-expanded, aria-controls, aria-autocomplete="list") and the results are a listbox of options grouped with role="group" and a heading.
- Focus stays in the field: ArrowUp and ArrowDown (or Ctrl+P and Ctrl+N) move the active option, announced through aria-activedescendant; PageUp and PageDown jump five; Enter runs it.
- The active option scrolls into view and has aria-selected="true".
- When nothing matches, the empty message is announced as a status.
- In CommandDialog, Escape closes the palette and focus returns to whatever opened it.