CommandPalette
A centred floating dialog with a search input and a list of commands. Use it as the top-level “do anything” surface in an editor or app — open it with a global hotkey, type to filter, and press Enter to run. The component handles fuzzy matching, keyboard navigation, grouping, and recent-item tracking; the consumer wires up the hotkey and the command handlers.
Live Preview
When to use
Section titled “When to use”- CommandPalette — searchable surface for any action in the app. Lives outside the chrome and is summoned by a hotkey.
- Menu — short, fixed list of actions anchored to a trigger. No search.
- ContextMenu — actions scoped to a right-clicked target.
Reach for CommandPalette once your command set grows past what fits in a menu, or when keyboard-first users need to find things by name.
Import
Section titled “Import”import { CommandPalette } from 'entangle-ui';import type { CommandItem } from 'entangle-ui';The fuzzy-matching helpers are exported too — useful if you want to power a custom UI with the same scoring:
import { fuzzyFilter, fuzzyScore } from 'entangle-ui';CommandPalette is fully controlled. Hold the open state in the parent and bind a hotkey to toggle it:
const [open, setOpen] = useState(false);
useHotkey('Mod+K', () => { setOpen(true);});
<CommandPalette open={open} onClose={() => setOpen(false)} items={[ { id: 'open', label: 'Open File', shortcut: 'Cmd+O', group: 'File' }, { id: 'save', label: 'Save', shortcut: 'Cmd+S', group: 'File' }, ]} onSelect={item => runCommand(item.id)} recentKey="myapp:command-palette"/>;The component does not bind a global hotkey itself — that belongs in the consumer so the binding can be configured, disabled in modals, or swapped to match the host application’s conventions.
Binding a global hotkey
Section titled “Binding a global hotkey”Wire the open state to useHotkey('Mod+K', ...) (or your hotkey library of choice). The example below uses a raw keydown listener for portability.
With a global hotkey
useEffect(() => { function handler(e: KeyboardEvent) { const isCmdK = (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k'; if (isCmdK) { e.preventDefault(); setOpen(o => !o); } } window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler);}, []);Fuzzy filtering
Section titled “Fuzzy filtering”Typing in the input fuzzy-matches against each item’s label, description, and keywords. The matcher scores subsequence hits and rewards matches that land on word boundaries — so “of” matches “Open File” higher than “Preof”. The filter re-sorts the list by score on every keystroke, and is wrapped in React’s useDeferredValue so the input stays responsive while large command lists re-filter.
<CommandPalette items={[ { id: 'theme', label: 'Switch Theme', group: 'Preferences', keywords: ['dark', 'light', 'appearance'], }, ]}/>Groups
Section titled “Groups”Each item’s group field becomes a section header. Items without a group go into an unlabelled default section that renders first.
Grouped commands
<CommandPalette items={[ { id: 'open', label: 'Open File', group: 'File' }, { id: 'save', label: 'Save', group: 'File' }, { id: 'find', label: 'Find in Files', group: 'Search' }, ]}/>Recent items
Section titled “Recent items”Pass a recentKey to track the last few selections in localStorage. When the input is empty, recents render in their own group at the top of the list. The component falls back gracefully when storage is unavailable (private browsing, SSR).
<CommandPalette recentKey="myapp:command-palette" maxRecent={5} recentLabel="Recent"/>Omit recentKey to disable tracking entirely.
Keyboard shortcuts on items
Section titled “Keyboard shortcuts on items”Pass a shortcut string to render a trailing <Kbd> chip. The shortcut is for display only — it doesn’t bind a global key, so the consumer is still responsible for wiring it up at the application level.
const items = [ { id: 'save', label: 'Save', shortcut: 'Cmd+S' }, { id: 'find', label: 'Find in Files', shortcut: 'Shift+Cmd+F' },];Empty state
Section titled “Empty state”Provide a custom emptyState node, or let the default “No matches for …” message render.
Empty state
<CommandPalette items={[]} emptyState={<span>Nothing to do here yet.</span>} />Custom item renderer
Section titled “Custom item renderer”For richer rows — multi-line descriptions, badges, inline previews — pass renderItem to replace the default layout. The renderer receives the item and a { selected, query } state object.
Custom row renderer
<CommandPalette items={items} renderItem={(item, state) => ( <div style={{ display: 'flex', gap: 12, fontWeight: state.selected ? 600 : 400, }} > <span style={{ flex: 1 }}>{item.label}</span> {item.shortcut && <span>{item.shortcut}</span>} </div> )}/>Item handlers
Section titled “Item handlers”Each item can supply its own onSelect callback for a one-shot action. When omitted, the top-level onSelect on CommandPalette is called with the item.
<CommandPalette items={[ { id: 'open', label: 'Open File', onSelect: () => openFileDialog() }, { id: 'save', label: 'Save' }, ]} onSelect={item => runCommand(item.id)}/>Use disabled: true on an item to render it but skip it during keyboard navigation and click.
Accessibility
Section titled “Accessibility”- The dialog uses
role="dialog"witharia-modal="true"andaria-label="Command palette". - The input is a
role="combobox"witharia-controlspointing at the listbox andaria-activedescendantreflecting the highlighted item. - The list is a
role="listbox"ofrole="option"items; the active option hasaria-selected="true". - Keyboard:
ArrowUp/ArrowDownto navigate,Home/Endto jump,Enterto run,Escapeto close. Hover mirrors keyboard selection. - Disabled items receive
aria-disabled="true"and are skipped by keyboard navigation. - Focus moves to the input on open. The consumer is responsible for restoring focus to the trigger on close.
API Reference
Section titled “API Reference”<CommandPalette>
Section titled “<CommandPalette>”| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Whether the palette is open. |
onClose | () => void | — | Called when the palette requests close (Escape, overlay click, after selection). |
items | readonly CommandItem[] | — | All commands. The component filters them as the user types. |
onSelect | (item: CommandItem) => void | — | Default handler invoked when an item without its own `onSelect` is chosen. |
placeholder | string | 'Type a command or search...' | Placeholder text for the search input. |
emptyState | ReactNode | — | Custom node rendered when no items match the current query. |
recentKey | string | — | `localStorage` key for tracking recently selected items. Recent tracking is off when omitted. |
recentLabel | string | 'Recent' | Localised label for the recent items group header. |
maxRecent | number | 5 | Maximum number of recent items to track. |
renderItem | (item: CommandItem, state: { selected: boolean; query: string }) => ReactNode | — | Custom renderer for each item — replaces the default row layout. |
portal | boolean | true | Render in a portal on `document.body`. |
maxHeight | number | 400 | Maximum height of the results list, in px. |
width | number | string | 640 | Width of the panel. Number → px, string → CSS value. |
CommandItem
Section titled “CommandItem”| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Stable unique identifier — used for keys and the recent list. |
label | string | — | Visible label. |
description | string | — | Optional secondary description rendered below the label. |
group | string | — | Group name for sectioning the list. Items without a group fall into the default section. |
icon | ReactNode | — | Leading icon rendered before the label. |
shortcut | string | — | Keyboard shortcut to display (e.g. "Cmd+S"). Rendered as a `<Kbd>` chip. |
keywords | string[] | — | Extra strings used by the fuzzy matcher in addition to label and description. |
disabled | boolean | — | When true, the item is rendered but cannot be selected and is skipped by keyboard navigation. |
onSelect | () => void | — | Callback fired when this item is chosen. Falls back to the top-level `onSelect`. |
Helpers
Section titled “Helpers”fuzzyFilter(query, items, getStrings) and fuzzyScore(query, target) are exported for building custom search UIs that match the palette’s behaviour. fuzzyFilter returns { item, score }[] sorted by descending score, with non-matches dropped.