Command Palette

A keyboard-first command palette with search, keyboard navigation, shortcuts, and grouped commands. Essential for power users and productivity apps.

SearchKeyboard NavShortcutsGrouped

Installation

Terminal
ReactTailwind
TSX
npx @uiblox/cli add command-palette

Preview

ESC

Actions

New Document

Create a new document

⌘N

Open File

Open an existing file

⌘O

Navigation

Search Files

Search across all files

⌘P

Go to Dashboard

Navigate to dashboard

Settings

Toggle Dark Mode

Switch theme

⌘D
↑↓ Navigate Select

Use Cases

Developer Tools

Quick access to IDE-like commands, file switching, and actions.

Productivity Apps

Enable power users to navigate and perform actions without mouse.

Dashboard Navigation

Quick navigation across complex dashboards and admin panels.

Search Everything

Unified search across pages, actions, settings, and content.

Source Code

command-palette.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"use client"; import { useState, useEffect, useMemo } from "react"; import { cn } from "@/lib/utils"; interface Command { id: string; title: string; description?: string; icon: React.ReactNode; shortcut?: string; category: string; action: () => void; } const commands: Command[] = [ { id: "1", title: "New Document", description: "Create a new document", icon: <FileIcon />, shortcut: "⌘N", category: "Actions", action: () => {} }, { id: "2", title: "Open File", description: "Open an existing file", icon: <FolderIcon />, shortcut: "⌘O", category: "Actions", action: () => {} }, { id: "3", title: "Save", description: "Save current document", icon: <SaveIcon />, shortcut: "⌘S", category: "Actions", action: () => {} }, { id: "4", title: "Search Files", description: "Search across all files", icon: <SearchIcon />, shortcut: "⌘P", category: "Navigation", action: () => {} }, { id: "5", title: "Go to Dashboard", description: "Navigate to dashboard", icon: <HomeIcon />, category: "Navigation", action: () => {} }, { id: "6", title: "Go to Settings", description: "Open settings page", icon: <SettingsIcon />, category: "Navigation", action: () => {} }, { id: "7", title: "Profile Settings", description: "Edit your profile", icon: <UserIcon />, category: "Settings", action: () => {} }, { id: "8", title: "Toggle Dark Mode", description: "Switch theme", icon: <MoonIcon />, shortcut: "⌘D", category: "Settings", action: () => {} }, { id: "9", title: "Keyboard Shortcuts", description: "View all shortcuts", icon: <KeyboardIcon />, shortcut: "⌘/", category: "Help", action: () => {} }, { id: "10", title: "Documentation", description: "Open docs", icon: <BookIcon />, category: "Help", action: () => {} }, ]; export function CommandPalette() { const [isOpen, setIsOpen] = useState(true); const [query, setQuery] = useState(""); const [selectedIndex, setSelectedIndex] = useState(0); const filteredCommands = useMemo(() => { if (!query) return commands; return commands.filter( (cmd) => cmd.title.toLowerCase().includes(query.toLowerCase()) || cmd.description?.toLowerCase().includes(query.toLowerCase()) ); }, [query]); const groupedCommands = useMemo(() => { const groups: Record<string, Command[]> = {}; filteredCommands.forEach((cmd) => { if (!groups[cmd.category]) groups[cmd.category] = []; groups[cmd.category].push(cmd); }); return groups; }, [filteredCommands]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setIsOpen(true); } if (e.key === "Escape") { setIsOpen(false); } if (e.key === "ArrowDown") { e.preventDefault(); setSelectedIndex((i) => Math.min(i + 1, filteredCommands.length - 1)); } if (e.key === "ArrowUp") { e.preventDefault(); setSelectedIndex((i) => Math.max(i - 1, 0)); } if (e.key === "Enter" && filteredCommands[selectedIndex]) { filteredCommands[selectedIndex].action(); setIsOpen(false); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [filteredCommands, selectedIndex]); useEffect(() => { setSelectedIndex(0); }, [query]); if (!isOpen) return null; return ( <div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh]"> {/* Backdrop */} <div className="absolute inset-0 bg-slate-900/50 backdrop-blur-sm" onClick={() => setIsOpen(false)} /> {/* Modal */} <div className="relative w-full max-w-xl bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 overflow-hidden"> {/* Search Input */} <div className="flex items-center gap-3 px-4 border-b border-slate-200 dark:border-slate-800"> <SearchIcon className="text-slate-400" /> <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Type a command or search..." className="flex-1 py-4 bg-transparent text-lg focus:outline-none text-slate-900 dark:text-white placeholder-slate-400" autoFocus /> <kbd className="px-2 py-1 text-xs font-medium bg-slate-100 dark:bg-slate-800 text-slate-500 rounded">ESC</kbd> </div> {/* Results */} <div className="max-h-96 overflow-y-auto py-2"> {filteredCommands.length === 0 ? ( <div className="px-4 py-8 text-center"> <p className="text-slate-500">No results found for "{query}"</p> </div> ) : ( Object.entries(groupedCommands).map(([category, cmds]) => ( <div key={category}> <div className="px-4 py-2"> <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide">{category}</p> </div> {cmds.map((cmd, idx) => { const globalIdx = filteredCommands.indexOf(cmd); return ( <button key={cmd.id} onClick={() => { cmd.action(); setIsOpen(false); }} onMouseEnter={() => setSelectedIndex(globalIdx)} className={cn( "w-full flex items-center gap-3 px-4 py-3 text-left transition-colors", selectedIndex === globalIdx ? "bg-purple-50 dark:bg-purple-900/20" : "hover:bg-slate-50 dark:hover:bg-slate-800/50" )} > <div className={cn( "w-10 h-10 rounded-xl flex items-center justify-center", selectedIndex === globalIdx ? "bg-purple-100 dark:bg-purple-900/30 text-purple-600" : "bg-slate-100 dark:bg-slate-800 text-slate-500" )}> {cmd.icon} </div> <div className="flex-1"> <p className={cn( "font-medium", selectedIndex === globalIdx ? "text-purple-600" : "text-slate-900 dark:text-white" )}> {cmd.title} </p> {cmd.description && ( <p className="text-sm text-slate-500">{cmd.description}</p> )} </div> {cmd.shortcut && ( <kbd className="px-2 py-1 text-xs font-medium bg-slate-100 dark:bg-slate-800 text-slate-500 rounded"> {cmd.shortcut} </kbd> )} </button> ); })} </div> )) )} </div> {/* Footer */} <div className="flex items-center justify-between px-4 py-3 border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-800/50"> <div className="flex items-center gap-4 text-xs text-slate-500"> <span className="flex items-center gap-1"><kbd className="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-700 rounded"></kbd> Navigate</span> <span className="flex items-center gap-1"><kbd className="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-700 rounded"></kbd> Select</span> <span className="flex items-center gap-1"><kbd className="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-700 rounded">esc</kbd> Close</span> </div> </div> </div> </div> ); } function SearchIcon({ className }: { className?: string }) { return <svg className={cn("w-5 h-5", className)} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>; } function FileIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>; } function FolderIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>; } function SaveIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" /></svg>; } function HomeIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>; } function SettingsIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>; } function UserIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>; } function MoonIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>; } function KeyboardIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /></svg>; } function BookIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>; }