Command palette (⌘K)
A searchable list of actions. Keyboard-driven: arrow keys to navigate, Enter to run, Esc to dismiss.
→ no command run yet
The Callable
declared once, mounted in the React treeimport { useEffect, useMemo, useRef, useState } from 'react'import { createCallable } from 'react-call'
export interface Command { id: string label: string shortcut?: string group?: string}
interface Props { commands: readonly Command[]}
export const CommandPalette = createCallable<Props, string | null>( ({ call, commands }) => { const [query, setQuery] = useState('') const [active, setActive] = useState(0) const listRef = useRef<HTMLUListElement>(null) const inputRef = useRef<HTMLInputElement>(null) const paletteRef = useRef<HTMLDivElement>(null)
const filtered = useMemo(() => { const q = query.trim().toLowerCase() if (!q) return commands return commands.filter((c) => c.label.toLowerCase().includes(q)) }, [commands, query])
useEffect(() => { inputRef.current?.focus() setActive(0) }, [])
useEffect(() => { const onPointer = (e: MouseEvent) => { if ( paletteRef.current && !paletteRef.current.contains(e.target as Node) ) { call.end(null) } } const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') call.end(null) if (e.key === 'ArrowDown') { e.preventDefault() setActive((a) => Math.min(filtered.length - 1, a + 1)) } if (e.key === 'ArrowUp') { e.preventDefault() setActive((a) => Math.max(0, a - 1)) } if (e.key === 'Enter') { e.preventDefault() const item = filtered[active] if (item) call.end(item.id) } } document.addEventListener('mousedown', onPointer) document.addEventListener('keydown', onKey) return () => { document.removeEventListener('mousedown', onPointer) document.removeEventListener('keydown', onKey) } }, [call, filtered, active])
return ( <div role="dialog" aria-modal="true" aria-label="Command palette" className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 px-4 pt-32 backdrop-blur-sm" > <div ref={paletteRef} className="w-full max-w-md overflow-hidden rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] shadow-2xl" > <input ref={inputRef} type="text" placeholder="Type a command…" value={query} onChange={(e) => setQuery(e.target.value)} className="w-full border-b border-[var(--color-border)] bg-transparent px-4 py-3 text-sm text-[var(--color-fg)] focus:outline-none" /> <ul ref={listRef} className="max-h-72 overflow-y-auto p-1"> {filtered.length === 0 ? ( <li className="px-3 py-2 text-sm text-[var(--color-fg-subtle)]"> No matches </li> ) : ( filtered.map((cmd, i) => ( <li key={cmd.id}> <button type="button" onMouseEnter={() => setActive(i)} onClick={() => call.end(cmd.id)} className={ i === active ? 'flex w-full items-center justify-between gap-3 rounded-md bg-[var(--color-bg-subtle)] px-3 py-2 text-left text-sm text-[var(--color-fg)]' : 'flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-left text-sm text-[var(--color-fg-muted)]' } > <span>{cmd.label}</span> {cmd.shortcut && ( <span className="font-mono text-xs text-[var(--color-fg-subtle)]"> {cmd.shortcut} </span> )} </button> </li> )) )} </ul> </div> </div> ) },)CommandPalette.displayName = 'CommandPalette'The Root
mounted in your app tree, onceimport { CommandPalette } from './CommandPalette'import { CommandPaletteTrigger } from './CommandPaletteTrigger'
export default function App() { return ( <> <CommandPalette /> <CommandPaletteTrigger /> </> )}The caller
anywhere in your app, imperativeimport { useState } from 'react'import { CommandPalette } from './CommandPalette'
const COMMANDS = [ { id: 'new-file', label: 'New file', shortcut: '⌘ N' }, { id: 'open', label: 'Open…', shortcut: '⌘ O' }, { id: 'save', label: 'Save', shortcut: '⌘ S' }, { id: 'find', label: 'Find in files', shortcut: '⌘ ⇧ F' }, { id: 'toggle-theme', label: 'Toggle theme' }, { id: 'restart', label: 'Restart' },] as const
export const CommandPaletteTrigger = () => { const [last, setLast] = useState<string | null>(null)
const handleClick = async () => { const id = await CommandPalette.call({ commands: COMMANDS }) if (id) setLast(id) }
return ( <div className="flex flex-col items-center gap-3"> <button type="button" onClick={handleClick} className="rounded-md border border-[var(--color-border-strong)] bg-[var(--color-bg)] px-4 py-2 font-mono text-sm text-[var(--color-fg)] transition-colors hover:bg-[var(--color-bg-muted)]" > ⌘ K </button> <span className="font-mono text-xs text-[var(--color-fg-subtle)]"> {last ? ( <span className="text-[var(--color-accent)]">→ {last}</span> ) : ( '→ no command run yet' )} </span> </div> )}