CommandPalette
Keyboard-driven command palette widget for quick actions
CommandPalette
The CommandPalette widget provides a keyboard-driven interface for executing commands, navigating, and performing actions. It's typically triggered with Cmd+K and supports fuzzy search, keyboard shortcuts, and recent command history.
Type Signature
interface CommandPaletteProps {
/** Available commands */
commands: Array<{
id: string
name: string
description?: string
icon?: string
shortcut?: string
onExecute?: () => void
category?: string
}>
/** Search query */
query?: string
/** Query change handler */
onQueryChange?: (query: string) => void
/** Recent commands */
recentCommands?: string[]
/** Show keyboard shortcuts */
showShortcuts?: boolean
/** Loading state */
loading?: boolean
/** Placeholder text */
placeholder?: string
}Usage
<CommandPalette
commands={[
{
id: 'new-key',
name: 'Create New API Key',
description: 'Generate a new API key',
icon: 'Plus',
category: 'API Keys',
shortcut: 'Cmd+N',
onExecute: () => createApiKey(),
},
{
id: 'settings',
name: 'Open Settings',
description: 'Configure your account',
icon: 'Settings',
category: 'Navigation',
shortcut: 'Cmd+,',
onExecute: () => navigate('/settings'),
},
]}
placeholder="Type a command or search..."
showShortcuts={true}
/>Props
commands
Array of available commands. Each command has a name, optional description, icon, keyboard shortcut, and execution handler.
<CommandPalette
commands={[
// Navigation commands
{
id: 'nav-dashboard',
name: 'Go to Dashboard',
category: 'Navigation',
icon: 'LayoutDashboard',
shortcut: 'G D',
onExecute: () => navigate('/'),
},
{
id: 'nav-keys',
name: 'Go to API Keys',
category: 'Navigation',
icon: 'Key',
shortcut: 'G K',
onExecute: () => navigate('/keys'),
},
// Action commands
{
id: 'create-key',
name: 'Create New API Key',
category: 'Actions',
icon: 'Plus',
shortcut: 'Cmd+N',
onExecute: () => createApiKey(),
},
{
id: 'export-data',
name: 'Export Data',
category: 'Actions',
icon: 'Download',
onExecute: () => exportData(),
},
]}
/>query
Current search query value.
<CommandPalette
query={query}
onQueryChange={setQuery}
commands={commands}
/>onQueryChange
Callback when the search query changes.
<CommandPalette
onQueryChange={(query) => {
setQuery(query)
// Filter commands based on query
filterCommands(query)
}}
/>recentCommands
Array of recently executed command IDs to show when query is empty.
<CommandPalette
commands={commands}
recentCommands={['nav-dashboard', 'create-key', 'settings']}
/>showShortcuts
Display keyboard shortcuts next to commands. Defaults to true.
<CommandPalette
commands={commands}
showShortcuts={true}
/>placeholder
Placeholder text in the search input.
<CommandPalette placeholder="What do you want to do?" />Widget Context Usage
CommandPalette is typically used as a modal overlay:
Modal Context
Full-screen command palette:
function App() {
const [paletteOpen, setPaletteOpen] = useState(false)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
setPaletteOpen(true)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
return (
<>
<Shell>
<DashboardView />
</Shell>
<Dialog open={paletteOpen} onClose={() => setPaletteOpen(false)}>
<CommandPalette
commands={commands}
onExecute={(command) => {
command.onExecute?.()
setPaletteOpen(false)
}}
/>
</Dialog>
</>
)
}Popup Context
Quick command menu:
<Popup
trigger={<Button size="icon"><Command /></Button>}
position="bottom"
>
<CommandPalette
commands={quickCommands}
placeholder="Quick action..."
/>
</Popup>Command Categories
Organize commands into categories:
<CommandPalette
commands={[
// Navigation
{
id: 'nav-dashboard',
name: 'Dashboard',
category: 'Navigation',
onExecute: () => navigate('/'),
},
{
id: 'nav-keys',
name: 'API Keys',
category: 'Navigation',
onExecute: () => navigate('/keys'),
},
// Create
{
id: 'create-key',
name: 'New API Key',
category: 'Create',
onExecute: () => createKey(),
},
{
id: 'create-webhook',
name: 'New Webhook',
category: 'Create',
onExecute: () => createWebhook(),
},
// Settings
{
id: 'settings-account',
name: 'Account Settings',
category: 'Settings',
onExecute: () => navigate('/settings/account'),
},
{
id: 'settings-team',
name: 'Team Settings',
category: 'Settings',
onExecute: () => navigate('/settings/team'),
},
]}
/>Common Patterns
Navigation Commands
Quick navigation to pages:
const navigationCommands = [
{
id: 'nav-dashboard',
name: 'Dashboard',
icon: 'LayoutDashboard',
shortcut: 'G D',
category: 'Go to',
onExecute: () => navigate('/'),
},
{
id: 'nav-keys',
name: 'API Keys',
icon: 'Key',
shortcut: 'G K',
category: 'Go to',
onExecute: () => navigate('/keys'),
},
{
id: 'nav-webhooks',
name: 'Webhooks',
icon: 'Webhook',
shortcut: 'G W',
category: 'Go to',
onExecute: () => navigate('/webhooks'),
},
{
id: 'nav-usage',
name: 'Usage',
icon: 'BarChart',
shortcut: 'G U',
category: 'Go to',
onExecute: () => navigate('/usage'),
},
{
id: 'nav-settings',
name: 'Settings',
icon: 'Settings',
shortcut: 'G S',
category: 'Go to',
onExecute: () => navigate('/settings'),
},
]Action Commands
Quick actions and operations:
const actionCommands = [
{
id: 'new-key',
name: 'Create API Key',
description: 'Generate a new API key',
icon: 'Plus',
shortcut: 'Cmd+N',
category: 'Actions',
onExecute: () => createApiKey(),
},
{
id: 'new-webhook',
name: 'Create Webhook',
description: 'Set up a new webhook endpoint',
icon: 'Plus',
category: 'Actions',
onExecute: () => createWebhook(),
},
{
id: 'export',
name: 'Export Data',
description: 'Download your data',
icon: 'Download',
category: 'Actions',
onExecute: () => exportData(),
},
{
id: 'invite',
name: 'Invite Team Member',
description: 'Send an invitation',
icon: 'UserPlus',
shortcut: 'Cmd+I',
category: 'Actions',
onExecute: () => inviteUser(),
},
]Theme Commands
Toggle theme and appearance:
const themeCommands = [
{
id: 'theme-light',
name: 'Light Theme',
icon: 'Sun',
category: 'Theme',
onExecute: () => setTheme('light'),
},
{
id: 'theme-dark',
name: 'Dark Theme',
icon: 'Moon',
category: 'Theme',
onExecute: () => setTheme('dark'),
},
{
id: 'theme-system',
name: 'System Theme',
icon: 'Monitor',
category: 'Theme',
onExecute: () => setTheme('system'),
},
]Help Commands
Access help and documentation:
const helpCommands = [
{
id: 'help-docs',
name: 'Documentation',
description: 'View API documentation',
icon: 'Book',
category: 'Help',
onExecute: () => window.open('/docs', '_blank'),
},
{
id: 'help-support',
name: 'Contact Support',
description: 'Get help from our team',
icon: 'MessageSquare',
category: 'Help',
onExecute: () => openSupport(),
},
{
id: 'help-changelog',
name: 'What\'s New',
description: 'See recent updates',
icon: 'Sparkles',
category: 'Help',
onExecute: () => showChangelog(),
},
]Keyboard Shortcuts
Register global keyboard shortcuts:
function CommandPaletteWithShortcuts() {
const [open, setOpen] = useState(false)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Cmd+K to open palette
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
setOpen(true)
return
}
// Global shortcuts (when palette is closed)
if (!open) {
// G + D = Dashboard
if (e.key === 'g') {
e.preventDefault()
const next = (ne: KeyboardEvent) => {
if (ne.key === 'd') navigate('/')
if (ne.key === 'k') navigate('/keys')
if (ne.key === 's') navigate('/settings')
window.removeEventListener('keydown', next)
}
window.addEventListener('keydown', next)
}
// Cmd+N = New API Key
if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
e.preventDefault()
createApiKey()
}
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [open])
return (
<CommandPalette
open={open}
onClose={() => setOpen(false)}
commands={commands}
/>
)
}Recent Commands
Track and display recently used commands:
function CommandPaletteWithRecent() {
const [recentCommands, setRecentCommands] = useState<string[]>([])
const addRecentCommand = (commandId: string) => {
setRecentCommands((prev) => {
const filtered = prev.filter((id) => id !== commandId)
return [commandId, ...filtered].slice(0, 5)
})
}
return (
<CommandPalette
commands={commands}
recentCommands={recentCommands}
onExecute={(command) => {
command.onExecute?.()
addRecentCommand(command.id)
}}
/>
)
}Fuzzy Search
Implement fuzzy command search:
function FuzzyCommandPalette() {
const [query, setQuery] = useState('')
const fuzzyMatch = (command: Command, query: string) => {
const commandText = `${command.name} ${command.description || ''}`.toLowerCase()
const searchText = query.toLowerCase()
// Simple fuzzy matching
let searchIndex = 0
for (let i = 0; i < commandText.length; i++) {
if (commandText[i] === searchText[searchIndex]) {
searchIndex++
if (searchIndex === searchText.length) return true
}
}
return false
}
const filteredCommands = commands.filter((cmd) =>
query.length === 0 ? true : fuzzyMatch(cmd, query)
)
return (
<CommandPalette
query={query}
onQueryChange={setQuery}
commands={filteredCommands}
/>
)
}Command Scoring
Rank commands by relevance:
const scoreCommand = (command: Command, query: string) => {
const name = command.name.toLowerCase()
const q = query.toLowerCase()
// Exact match
if (name === q) return 100
// Starts with query
if (name.startsWith(q)) return 80
// Contains query
if (name.includes(q)) return 60
// Fuzzy match
if (fuzzyMatch(command, query)) return 40
return 0
}
const sortedCommands = commands
.map((cmd) => ({
command: cmd,
score: scoreCommand(cmd, query),
}))
.filter((item) => item.score > 0)
.sort((a, b) => b.score - a.score)
.map((item) => item.command)Dynamic Commands
Generate commands dynamically:
function DynamicCommandPalette() {
const { user, projects } = useData()
const commands = useMemo(() => {
const staticCommands = [
{ id: 'nav-dashboard', name: 'Dashboard', ... },
{ id: 'new-key', name: 'Create API Key', ... },
]
// Add project navigation commands
const projectCommands = projects.map((project) => ({
id: `project-${project.id}`,
name: `Go to ${project.name}`,
category: 'Projects',
icon: 'Folder',
onExecute: () => navigate(`/projects/${project.id}`),
}))
// Add user-specific commands
const userCommands = user.isAdmin
? [
{
id: 'admin-panel',
name: 'Admin Panel',
category: 'Admin',
icon: 'Shield',
onExecute: () => navigate('/admin'),
},
]
: []
return [...staticCommands, ...projectCommands, ...userCommands]
}, [user, projects])
return <CommandPalette commands={commands} />
}Command Groups
Organize commands into collapsible groups:
<CommandPalette
commands={commands}
renderGroups={(commands) => {
const grouped = commands.reduce((acc, cmd) => {
const category = cmd.category || 'Other'
if (!acc[category]) acc[category] = []
acc[category].push(cmd)
return acc
}, {})
return Object.entries(grouped).map(([category, cmds]) => (
<div key={category}>
<h3 className="text-sm font-semibold mb-2">{category}</h3>
<CommandList commands={cmds} />
</div>
))
}}
/>Empty States
Handle empty states:
<CommandPalette
query={query}
commands={filteredCommands}
emptyState={
query.length === 0 ? (
<div className="text-center py-8">
<p className="text-muted-foreground">
Type to search commands...
</p>
</div>
) : (
<div className="text-center py-8">
<p className="text-muted-foreground">
No commands found for "{query}"
</p>
</div>
)
}
/>Command Palette + Search
Combine command execution with search:
function UnifiedPalette() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
useEffect(() => {
if (query.startsWith('>')) {
// Command mode
const commandQuery = query.slice(1)
setResults(commands.filter((c) =>
c.name.toLowerCase().includes(commandQuery.toLowerCase())
))
} else {
// Search mode
performSearch(query).then(setResults)
}
}, [query])
return (
<CommandPalette
query={query}
onQueryChange={setQuery}
results={results}
placeholder={
query.startsWith('>') ? 'Type a command...' : 'Search or type > for commands...'
}
/>
)
}Accessibility
CommandPalette implements accessibility best practices:
- ARIA labels - Descriptive labels for screen readers
- Keyboard navigation - Full keyboard support
- Focus management - Proper focus handling
- Live regions - Result updates announced
<div role="dialog" aria-modal="true" aria-label="Command Palette">
<input
type="text"
role="combobox"
aria-expanded="true"
aria-controls="command-list"
aria-activedescendant="command-1"
/>
<ul id="command-list" role="listbox">
<li id="command-1" role="option" aria-selected="true">
Create API Key
<kbd>Cmd+N</kbd>
</li>
</ul>
</div>Best Practices
Use Clear Command Names
Make commands easy to find:
// Good - clear, searchable names
{ name: 'Create API Key', ... }
{ name: 'Export Data', ... }
{ name: 'Invite Team Member', ... }
// Bad - vague names
{ name: 'New', ... }
{ name: 'Download', ... }
{ name: 'Add User', ... }Add Descriptions
Help users understand what commands do:
{
name: 'Create API Key',
description: 'Generate a new API key for authentication',
...
}Group Related Commands
Organize commands by category:
commands.sort((a, b) => {
if (a.category !== b.category) {
return a.category.localeCompare(b.category)
}
return a.name.localeCompare(b.name)
})Show Keyboard Shortcuts
Display shortcuts for discoverability:
<CommandPalette
commands={commands}
showShortcuts={true}
/>Track Command Usage
Learn which commands are popular:
const trackCommandUsage = (commandId: string) => {
analytics.track('command_executed', {
command_id: commandId,
timestamp: new Date().toISOString(),
})
}Related Components
- DetailPanel - Detail side panel component
- ActivityPanel - Activity feed component
- SearchBox - Modal search widget
- ChatBox - Slideover chat widget