UX Standards — 16: Command Palette (⌘K / Ctrl+K)
Governs: The keyboard-triggered command palette for navigation and search.
Parent rules: See 00-OVERVIEW-AND-CSS-RULES.md and 02-HEADER.md first.
Overview
The Command Palette is a centered modal that opens when the user presses ⌘K (Mac) or Ctrl+K (Windows/Linux), or clicks the Search button in the header. It provides:
- Instant navigation to any page in the application
- Grouped navigation items matching the sidebar structure
- Keyboard-first UX with arrow-key navigation and Enter to select
The palette is built using the shadcn/ui Command component (which wraps cmdk).
Trigger
Keyboard Shortcut
| Platform | Shortcut |
|---|---|
| Mac | ⌘K |
| Windows / Linux | Ctrl+K |
Header Search Button
The Search button in the header toolbar (visible in HeaderRight) opens the palette on click:
// Header search button (registers keyboard shortcut at app level)
<Button variant="ghost" size="icon" className="h-9 w-9"
onClick={() => setCommandOpen(true)}>
<Search className="h-5 w-5" />
</Button>
Tooltip: The header button tooltip shows "Search (⌘K)" (or "Search (Ctrl+K)" on Windows).
Keyboard Registration (App Level)
// App-level keyboard shortcut registration:
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setCommandOpen((open) => !open);
}
};
document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
}, []);
Visual Anatomy
┌────────────────────────────────────────────────────────────┐
│ [🔍 Type a command or search... ] │ ← CommandInput
├────────────────────────────────────────────────────────────┤
│ Navigation │ ← CommandGroup heading
│ 📊 Dashboard │ ← CommandItem
│ 👥 Users │
│ 📋 Bookings │
├────────────────────────────────────────────────────────────┤
│ Settings │ ← CommandGroup heading
│ ⚙ General Settings │
│ 🎨 Appearance │
│ 👤 Profile │
├────────────────────────────────────────────────────────────┤
│ ↑↓ Navigate ↵ Select Esc Close │ ← Keyboard hints footer
└────────────────────────────────────────────────────────────┘
Container Spec
Built with shadcn/ui CommandDialog:
<CommandDialog open={commandOpen} onOpenChange={setCommandOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Navigation">
<CommandItem onSelect={() => { router.push('/dashboard'); setCommandOpen(false); }}>
<LayoutDashboard className="mr-2 h-4 w-4" />
Dashboard
</CommandItem>
{/* ... more items */}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem onSelect={() => { router.push('/settings/appearance'); setCommandOpen(false); }}>
<Palette className="mr-2 h-4 w-4" />
Appearance
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
| Property | Value |
|---|---|
| Component | CommandDialog (shadcn/ui — wraps cmdk) |
| Position | Centered (via Dialog backdrop) |
| Max width | max-w-lg (shadcn default) |
| Input | CommandInput placeholder="Type a command or search..." |
| Blocks page | Yes — backdrop overlay, but keyboard focus stays in palette |
| Close | Esc key or click backdrop |
Navigation Groups
The command palette groups items to match the sidebar navigation structure:
| Group Heading | Example Items |
|---|---|
| Navigation | Dashboard, Users, Bookings, Products, Reports |
| Settings | General Settings, Appearance, Profile, Notifications |
| (App-specific groups) | Per-module navigation items |
Each CommandGroup has a heading that displays as a small label above its items.
Command Item Pattern
<CommandItem
onSelect={() => {
router.push('/target-path');
setCommandOpen(false);
}}
>
<IconName className="mr-2 h-4 w-4" />
Label
</CommandItem>
| Element | Spec |
|---|---|
| Icon | h-4 w-4 mr-2 |
| Label | Plain text — the page/section name |
| On select | Navigate + close palette |
Keyboard Hints Footer
The modal shows keyboard hints at the bottom edge:
↑↓ Navigate ↵ Select Esc Close
These are displayed as text-xs text-muted-foreground in the command palette footer.
Empty State
When the search query has no matching items:
<CommandEmpty>No results found.</CommandEmpty>
Filtering Behavior
cmdk handles filtering automatically — items are filtered in real-time as the user types. The CommandInput value is matched against all CommandItem text content.
Violation Checklist
- Triggered by
⌘K/Ctrl+Kkeyboard shortcut registered at app level - Header Search button has tooltip showing
"Search (⌘K)"/"Search (Ctrl+K)" - Uses
CommandDialog(not a custom modal) - Input placeholder:
"Type a command or search..." - Items grouped with
CommandGroup headinglabels - Each item has a Lucide icon (
h-4 w-4 mr-2) - Selecting an item navigates and closes the palette
-
CommandEmptyshows"No results found." - Keyboard hints visible: ↑↓ Navigate, ↵ Select, Esc Close