Command
shadcn Command component — command palette, search, and keyboard-driven navigation powered by cmdk.
Recipe
Quick-reference recipe card — copy-paste ready.
npx shadcn@latest add command dialogimport {
Command, CommandDialog, CommandEmpty, CommandGroup,
CommandInput, CommandItem, CommandList, CommandSeparator,
} from "@/components/ui/command";
// Inline command menu
<Command>
<CommandInput placeholder="Search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Actions">
<CommandItem onSelect={() => console.log("new")}>New File</CommandItem>
<CommandItem onSelect={() => console.log("save")}>Save</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem>Profile</CommandItem>
<CommandItem>Preferences</CommandItem>
</CommandGroup>
</CommandList>
</Command>When to reach for this: When you want a keyboard-first search or action palette — Cmd+K style navigation, spotlight search, or combobox selection.
Working Example
"use client";
import { useEffect, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import {
CommandDialog, CommandEmpty, CommandGroup,
CommandInput, CommandItem, CommandList, CommandSeparator,
} from "@/components/ui/command";
import {
FileText, Settings, User, Search, Plus, Moon, Sun, LogOut,
} from "lucide-react";
type CommandAction = {
id: string;
label: string;
icon: React.ReactNode;
shortcut?: string;
action: () => void;
};
export function CommandPalette() {
const [open, setOpen] = useState(false);
const router = useRouter();
// Cmd+K to open
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((o) => !o);
}
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, []);
const runAndClose = useCallback(
(fn: () => void) => {
fn();
setOpen(false);
},
[]
);
const pages: CommandAction[] = [
{ id: "home", label: "Home", icon: <FileText className="mr-2 h-4 w-4" />, action: () => router.push("/") },
{ id: "dashboard", label: "Dashboard", icon: <FileText className="mr-2 h-4 w-4" />, action: () => router.push("/dashboard") },
{ id: "settings", label: "Settings", icon: <Settings className="mr-2 h-4 w-4" />, shortcut: "Cmd+,", action: () => router.push("/settings") },
{ id: "profile", label: "Profile", icon: <User className="mr-2 h-4 w-4" />, action: () => router.push("/profile") },
];
const actions: CommandAction[] = [
{ id: "new-doc", label: "New Document", icon: <Plus className="mr-2 h-4 w-4" />, shortcut: "Cmd+N", action: () => console.log("new doc") },
{ id: "search", label: "Search Everything", icon: <Search className="mr-2 h-4 w-4" />, shortcut: "Cmd+F", action: () => console.log("search") },
];
const theme: CommandAction[] = [
{ id: "light", label: "Light Mode", icon: <Sun className="mr-2 h-4 w-4" />, action: () => document.documentElement.classList.remove("dark") },
{ id: "dark", label: "Dark Mode", icon: <Moon className="mr-2 h-4 w-4" />, action: () => document.documentElement.classList.add("dark") },
];
return (
<>
{/* Trigger button */}
<button
onClick={() => setOpen(true)}
className="flex items-center gap-2 rounded-lg border bg-muted/50 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-muted"
>
<Search className="h-4 w-4" />
<span>Search...</span>
<kbd className="ml-4 rounded bg-muted px-1.5 py-0.5 text-xs font-mono">Cmd+K</kbd>
</button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Pages">
{pages.map((item) => (
<CommandItem key={item.id} onSelect={() => runAndClose(item.action)}>
{item.icon}
<span>{item.label}</span>
{item.shortcut && (
<kbd className="ml-auto text-xs text-muted-foreground">{item.shortcut}</kbd>
)}
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Actions">
{actions.map((item) => (
<CommandItem key={item.id} onSelect={() => runAndClose(item.action)}>
{item.icon}
<span>{item.label}</span>
{item.shortcut && (
<kbd className="ml-auto text-xs text-muted-foreground">{item.shortcut}</kbd>
)}
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Theme">
{theme.map((item) => (
<CommandItem key={item.id} onSelect={() => runAndClose(item.action)}>
{item.icon}
<span>{item.label}</span>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Account">
<CommandItem onSelect={() => runAndClose(() => console.log("logout"))}>
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</>
);
}What this demonstrates:
Cmd+Kkeyboard shortcut to openCommandDialogwrapping (renders in a Dialog)- Grouped commands with icons and keyboard shortcuts
onSelecthandlers that navigate or trigger actions- Close-on-select behavior
- Search input with automatic filtering
Deep Dive
How It Works
- Command is built on
cmdkby Paco Coursey — a headless, composable command menu CommandInputprovides a search box that automatically filtersCommandItemchildren by text contentCommandItemis keyboard-navigable — Arrow keys move between items, Enter selectsCommandDialogcombinesCommandwith shadcn'sDialogfor the modal overlayCommandGroupprovides visual grouping with an optional headingCommandEmptyrenders when no items match the search query- Filtering is client-side by default using the item's text content
Variations
Async search with loading:
function AsyncCommand() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<Result[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) { setResults([]); return; }
setLoading(true);
const timer = setTimeout(async () => {
const data = await fetch(`/api/search?q=${query}`).then((r) => r.json());
setResults(data);
setLoading(false);
}, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<Command shouldFilter={false}>
<CommandInput value={query} onValueChange={setQuery} />
<CommandList>
{loading && <CommandEmpty>Searching...</CommandEmpty>}
{!loading && results.length === 0 && <CommandEmpty>No results.</CommandEmpty>}
{results.map((r) => (
<CommandItem key={r.id} value={r.id} onSelect={() => navigate(r)}>
{r.title}
</CommandItem>
))}
</CommandList>
</Command>
);
}Combobox (select with search):
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
function Combobox({ options, value, onSelect }: {
options: { value: string; label: string }[];
value: string;
onSelect: (v: string) => void;
}) {
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" aria-expanded={open}>
{options.find((o) => o.value === value)?.label ?? "Select..."}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search..." />
<CommandList>
<CommandEmpty>No match.</CommandEmpty>
<CommandGroup>
{options.map((opt) => (
<CommandItem key={opt.value} value={opt.value} onSelect={() => { onSelect(opt.value); setOpen(false); }}>
{opt.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}TypeScript Notes
// CommandItem onSelect receives the lowercased value string
<CommandItem
value="unique-id"
onSelect={(value: string) => {
// value is lowercase of the item text or the explicit value prop
}}
>
// Typing command actions
interface CommandAction {
id: string;
label: string;
icon: React.ReactNode;
shortcut?: string;
action: () => void;
keywords?: string[]; // extra search terms
}Gotchas
-
onSelectvalue is lowercased — cmdk lowercases the value for matching. Fix: Use a separatevalueprop and look up the original data by ID. -
shouldFilter={false}for async — If you fetch results from an API, disable the built-in filter. Fix: SetshouldFilter={false}and handle filtering server-side. -
Items must have text content — cmdk filters by text content of the
CommandItem. If your item only has icons, filtering will not work. Fix: Add visible text or use thevalueprop for custom filtering. -
Dialog z-index —
CommandDialoguses the same z-index as Dialog. If you have other overlays, they may conflict. Fix: Adjust the z-index viaclassNameonCommandDialog. -
Keyboard shortcut conflicts —
Cmd+Kmay conflict with browser or editor shortcuts. Fix: Check for conflicts and document the shortcut for users.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| cmdk (bare) | You want the headless library without shadcn styling | You already use shadcn |
| Algolia DocSearch | You need full-text search across documentation | You need a general command palette |
| kbar | You want an alternative command palette with different API | cmdk covers your needs |
Native <datalist> | You need a simple browser-native autocomplete | You want rich keyboard navigation and grouping |
FAQs
What is the difference between Command and CommandDialog?
Commandrenders an inline command menu embedded in the pageCommandDialogwrapsCommandinside a shadcnDialogfor a modal overlay- Use
CommandDialogfor Cmd+K style palettes; useCommandfor inline comboboxes
How do you wire up a Cmd+K keyboard shortcut to open the command palette?
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((o) => !o);
}
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, []);What underlying library powers the shadcn Command component?
cmdkby Paco Coursey -- a headless, composable command menu library- It provides keyboard navigation (arrow keys, Enter to select) and built-in text filtering
Gotcha: Why does onSelect return a lowercased value?
- cmdk lowercases the value string internally for case-insensitive matching
- Fix: use a separate
valueprop onCommandItemand look up the original data by ID rather than relying on theonSelectstring
How do you build a combobox (searchable select) using Command?
- Wrap
Commandinside aPopoverinstead of a Dialog - Use
PopoverTriggeras the button andPopoverContentcontaining the Command - Close the popover on item selection with
setOpen(false)
When should you set shouldFilter={false} on Command?
- When fetching results from an API (async/server-side search)
- The built-in client-side filter would hide results that haven't loaded yet
- Handle filtering on the server and pass results directly to
CommandItem
How do you implement async search with a debounced API call?
useEffect(() => {
if (!query) { setResults([]); return; }
setLoading(true);
const timer = setTimeout(async () => {
const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
setResults(data);
setLoading(false);
}, 300);
return () => clearTimeout(timer);
}, [query]);What does CommandEmpty display and when?
- It renders its children when no
CommandItemelements match the current search query - Useful for "No results found" or "Searching..." messages
How do you type a command action for use in a dynamic command list?
interface CommandAction {
id: string;
label: string;
icon: React.ReactNode;
shortcut?: string;
action: () => void;
keywords?: string[];
}Gotcha: Why might filtering not work if a CommandItem only contains an icon?
- cmdk filters by the text content of each
CommandItem - If the item has no visible text, the filter has nothing to match against
- Fix: add visible text or set the
valueprop explicitly for custom filtering
How do you close the command palette after an item is selected?
const runAndClose = useCallback((fn: () => void) => {
fn();
setOpen(false);
}, []);
<CommandItem onSelect={() => runAndClose(item.action)}>What TypeScript type does onSelect expect on a CommandItem?
onSelecttakes a callback of type(value: string) => void- The
valueparameter is the lowercased text content or the explicitvalueprop
Related
- Dialog — CommandDialog uses Dialog internally
- Button — trigger buttons for the palette
- Data Table — filtering UI for tables
- Setup — installation