React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

shadcncommandcmdkcommand-palettesearch

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 dialog
import {
  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+K keyboard shortcut to open
  • CommandDialog wrapping (renders in a Dialog)
  • Grouped commands with icons and keyboard shortcuts
  • onSelect handlers that navigate or trigger actions
  • Close-on-select behavior
  • Search input with automatic filtering

Deep Dive

How It Works

  • Command is built on cmdk by Paco Coursey — a headless, composable command menu
  • CommandInput provides a search box that automatically filters CommandItem children by text content
  • CommandItem is keyboard-navigable — Arrow keys move between items, Enter selects
  • CommandDialog combines Command with shadcn's Dialog for the modal overlay
  • CommandGroup provides visual grouping with an optional heading
  • CommandEmpty renders 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

  • 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-indexCommandDialog 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 conflictsCmd+K may conflict with browser or editor shortcuts. Fix: Check for conflicts and document the shortcut for users.

Alternatives

AlternativeUse WhenDon't Use When
cmdk (bare)You want the headless library without shadcn stylingYou already use shadcn
Algolia DocSearchYou need full-text search across documentationYou need a general command palette
kbarYou want an alternative command palette with different APIcmdk covers your needs
Native <datalist>You need a simple browser-native autocompleteYou want rich keyboard navigation and grouping

FAQs

What is the difference between Command and CommandDialog?
  • Command renders an inline command menu embedded in the page
  • CommandDialog wraps Command inside a shadcn Dialog for a modal overlay
  • Use CommandDialog for Cmd+K style palettes; use Command for 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?
  • cmdk by 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 value prop on CommandItem and look up the original data by ID rather than relying on the onSelect string
How do you build a combobox (searchable select) using Command?
  • Wrap Command inside a Popover instead of a Dialog
  • Use PopoverTrigger as the button and PopoverContent containing 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 CommandItem elements 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 value prop 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?
  • onSelect takes a callback of type (value: string) => void
  • The value parameter is the lowercased text content or the explicit value prop
  • Dialog — CommandDialog uses Dialog internally
  • Button — trigger buttons for the palette
  • Data Table — filtering UI for tables
  • Setup — installation