Blox/
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
TSXReactTailwind
npx @uiblox/cli add command-palettePreview
ESC
Actions
New Document
Create a new document
Open File
Open an existing file
Navigation
Search Files
Search across all files
Go to Dashboard
Navigate to dashboard
Settings
Toggle Dark Mode
Switch theme
↑↓ 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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186"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>; }