Command
shadcn Command component - command palette, search, and keyboard-driven navigation powered by cmdk.
Search across all documentation pages
shadcn Command component - command palette, search, and keyboard-driven navigation powered by cmdk.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
"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+K keyboard shortcut to openCommandDialog wrapping (renders in a Dialog)onSelect handlers that navigate or trigger actionscmdk by Paco Coursey - a headless, composable command menuCommandInput provides a search box that automatically filters CommandItem children by text contentCommandItem is keyboard-navigable - Arrow keys move between items, Enter selectsCommandDialog combines Command with shadcn's Dialog for the modal overlayCommandGroup provides visual grouping with an optional headingCommandEmpty renders when no items match the search queryAsync 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>
);
}// 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
}onSelect value is lowercased - cmdk lowercases the value for matching. Fix: Use a separate value prop 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: Set shouldFilter={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 the value prop for custom filtering.
Dialog z-index - CommandDialog uses the same z-index as Dialog. If you have other overlays, they may conflict. Fix: Adjust the z-index via className on CommandDialog.
Keyboard shortcut conflicts - Cmd+K may conflict with browser or editor shortcuts. Fix: Check for conflicts and document the shortcut for users.
| 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 |
Command renders an inline command menu embedded in the pageCommandDialog wraps Command inside a shadcn Dialog for a modal overlayCommandDialog for Cmd+K style palettes; use Command for inline comboboxesuseEffect(() => {
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);
}, []);cmdk by Paco Coursey -- a headless, composable command menu libraryvalue prop on CommandItem and look up the original data by ID rather than relying on the onSelect stringCommand inside a Popover instead of a DialogPopoverTrigger as the button and PopoverContent containing the CommandsetOpen(false)CommandItemuseEffect(() => {
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]);CommandItem elements match the current search queryinterface CommandAction {
id: string;
label: string;
icon: React.ReactNode;
shortcut?: string;
action: () => void;
keywords?: string[];
}CommandItemvalue prop explicitly for custom filteringconst runAndClose = useCallback((fn: () => void) => {
fn();
setOpen(false);
}, []);
<CommandItem onSelect={() => runAndClose(item.action)}>onSelect takes a callback of type (value: string) => voidvalue parameter is the lowercased text content or the explicit value propReviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥