Skip to content
Entangle UI v0.13.0

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

  • 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 { 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.

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);
}, []);

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'],
},
]}
/>

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' },
]}
/>

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.

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' },
];

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>} />

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>
)}
/>

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.

  • The dialog uses role="dialog" with aria-modal="true" and aria-label="Command palette".
  • The input is a role="combobox" with aria-controls pointing at the listbox and aria-activedescendant reflecting the highlighted item.
  • The list is a role="listbox" of role="option" items; the active option has aria-selected="true".
  • Keyboard: ArrowUp / ArrowDown to navigate, Home / End to jump, Enter to run, Escape to 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.
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.
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`.

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.