Command Palette Implementation with Claude Code
Learn about command palette implementation using Claude Code. Practical tips and code examples included.
A Command Palette Can Dramatically Improve the User Experience
The command palettes you see in VS Code and Slack are power-user UI elements that let you operate the app entirely from the keyboard. With Claude Code, you can efficiently implement a command palette with search, filtering, and keyboard navigation.
Basic Command Palette Component
> Implement a command palette that opens with Cmd+K (Mac) / Ctrl+K (Windows).
> Support search, keyboard operation, and category grouping.
// components/CommandPalette.tsx
import { useState, useEffect, useRef, useCallback } from 'react';
interface Command {
id: string;
label: string;
category: string;
icon?: string;
shortcut?: string;
action: () => void;
}
interface CommandPaletteProps {
commands: Command[];
}
export function CommandPalette({ commands }: CommandPaletteProps) {
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const filtered = commands.filter(cmd =>
cmd.label.toLowerCase().includes(query.toLowerCase()) ||
cmd.category.toLowerCase().includes(query.toLowerCase())
);
// Global shortcut
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
setIsOpen(prev => !prev);
}
if (e.key === 'Escape') setIsOpen(false);
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
// Focus management
useEffect(() => {
if (isOpen) {
inputRef.current?.focus();
setQuery('');
setSelectedIndex(0);
}
}, [isOpen]);
const handleKeyNav = useCallback((e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(i => Math.min(i + 1, filtered.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(i => Math.max(i - 1, 0));
break;
case 'Enter':
e.preventDefault();
if (filtered[selectedIndex]) {
filtered[selectedIndex].action();
setIsOpen(false);
}
break;
}
}, [filtered, selectedIndex]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh]">
<div className="fixed inset-0 bg-black/50" onClick={() => setIsOpen(false)} />
<div className="relative w-full max-w-lg rounded-xl bg-white shadow-2xl dark:bg-gray-800">
<input
ref={inputRef}
type="text"
value={query}
onChange={e => { setQuery(e.target.value); setSelectedIndex(0); }}
onKeyDown={handleKeyNav}
placeholder="Search commands..."
className="w-full border-b px-4 py-3 text-lg outline-none dark:bg-gray-800"
/>
<ul className="max-h-80 overflow-y-auto p-2" role="listbox">
{filtered.map((cmd, index) => (
<li
key={cmd.id}
role="option"
aria-selected={index === selectedIndex}
onClick={() => { cmd.action(); setIsOpen(false); }}
className={`flex cursor-pointer items-center justify-between rounded-lg px-3 py-2
${index === selectedIndex ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50'}`}
>
<div className="flex items-center gap-3">
{cmd.icon && <span className="text-lg">{cmd.icon}</span>}
<div>
<div className="font-medium">{cmd.label}</div>
<div className="text-xs text-gray-500">{cmd.category}</div>
</div>
</div>
{cmd.shortcut && (
<kbd className="rounded bg-gray-100 px-2 py-1 text-xs dark:bg-gray-700">
{cmd.shortcut}
</kbd>
)}
</li>
))}
{filtered.length === 0 && (
<li className="px-3 py-8 text-center text-gray-500">
No matching commands
</li>
)}
</ul>
</div>
</div>
);
}
Defining and Registering Commands
// hooks/useCommands.ts
import { useRouter } from 'next/navigation';
export function useCommands(): Command[] {
const router = useRouter();
return [
{
id: 'home', label: 'Go to home', category: 'Navigation',
icon: '🏠', shortcut: 'G H',
action: () => router.push('/'),
},
{
id: 'blog', label: 'Blog list', category: 'Navigation',
icon: '📝', shortcut: 'G B',
action: () => router.push('/blog'),
},
{
id: 'theme', label: 'Toggle theme', category: 'Settings',
icon: '🌙', shortcut: 'T T',
action: () => document.documentElement.classList.toggle('dark'),
},
{
id: 'search', label: 'Search posts', category: 'Search',
icon: '🔍', shortcut: '/',
action: () => router.push('/search'),
},
];
}
Accessibility
// Apply WAI-ARIA
<div
role="dialog"
aria-modal="true"
aria-label="Command palette"
>
<input
role="combobox"
aria-expanded="true"
aria-controls="command-list"
aria-activedescendant={`command-${filtered[selectedIndex]?.id}`}
/>
<ul id="command-list" role="listbox">
{/* ... */}
</ul>
</div>
Summary
Combined with keyboard shortcuts, a command palette can dramatically boost power-user productivity. With Claude Code, you can implement a fully featured command palette with search, filtering, and keyboard navigation in no time. Pay attention to accessibility so that the UI is easy to use for everyone. You may also want to look at the cmdk library for reference.
Level up your Claude Code workflow
50 battle-tested prompt templates you can copy-paste into Claude Code right now.
Free PDF: Claude Code Cheatsheet in 5 Minutes
Key commands, shortcuts, and prompt examples on a single printable page.
About the Author
Masa
Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.
Related Posts
How to Supercharge Your Side Projects with Claude Code [With Examples]
How to Supercharge Your Side Projects with Claude Code [With Examples]. A practical guide with code examples.
How to Automate Refactoring with Claude Code
Learn how to automate refactoring using Claude Code. Includes practical code examples and step-by-step guidance.
Complete CORS Configuration Guide with Claude Code
A complete CORS configuration guide using Claude Code. Practical tips and code examples included.