Command Palette Pattern
Overview
Build performant, keyboard-first command palettes using cmdk (the industry standard) with shadcn/ui components.
Why cmdk + shadcn:
- De facto standard (Vercel, Linear, Raycast-style UIs)
- Built-in fuzzy search
- Accessible (Radix Dialog primitives)
- Keyboard-first design
Core Components
| Component | Purpose |
| --- | --- |
| CommandDialog | Modal wrapper (Dialog + Command) |
| CommandInput | Search input with icon |
| CommandList | Scrollable results container |
| CommandEmpty | "No results" state |
| CommandGroup | Categorized sections with headings |
| CommandItem | Individual selectable items |
| CommandSeparator | Visual divider between groups |
| CommandShortcut | Keyboard shortcut hint display |
Basic Structure
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Navigation">
<CommandItem onSelect={() => navigate('/home')}>
<HomeIcon /> Home
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
Keyboard Shortcuts
Standard bindings:
Cmd+K/Ctrl+K- Open/close (standard convention)/- Open (when not in input field)Escape- Close- Arrow keys - Navigate
Enter- Select
Implementation:
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
setOpen(prev => !prev);
}
if (e.key === '/' && !isInputFocused(e.target)) {
e.preventDefault();
setOpen(true);
}
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, []);
function isInputFocused(target: EventTarget | null): boolean {
return target instanceof HTMLElement &&
['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName);
}
Search Index Patterns
Static sites:
- Build index at compile time
- Pass as props to component
Dynamic:
- Fetch on mount or use server action
Structure:
interface SearchItem {
title: string;
description?: string;
href: string;
type: string; // e.g., "page", "blog", "command"
keywords?: string[];
}
cmdk search:
- Searches the
valueprop onCommandItem - Include searchable text in value:
value={title + ' ' + keywords?.join(' ')}
Styling Customization
Height:
<CommandList className="max-h-[300px]">
Dialog position:
- Centered by default via
DialogContent
Item states:
<CommandItem className="data-[selected=true]:bg-accent">
Accessibility
Built-in:
- Focus trap (Radix Dialog)
- Escape/click-outside handling
- Arrow navigation (cmdk)
- Screen reader announcements
Ensure:
- Visible focus indicators
- Meaningful item labels
- Icon-only items have aria-label
External Resources
- cmdk docs: https://cmdk.paco.me/
- shadcn Command: https://ui.shadcn.com/docs/components/command
- Radix Dialog: https://www.radix-ui.com/primitives/docs/components/dialog