CommandPalette
Dialog-based command list with search filtering and keyboard-shortcut hints.
import { CommandPalette } from '@elirobinson/react/components/organisms/CommandPalette';Styles: @elirobinson/react/styles/organisms/CommandPalette.css@elirobinson/react/styles/organisms/Dialog.css — already included when you import @elirobinson/react/styles.css.
Show code
import { useState } from 'react';
import { CommandPalette } from '@elirobinson/react/components/organisms/CommandPalette';
export default function Basic() {
const [open, setOpen] = useState(false);
return (
<div>
<button type="button" className="ds-button ds-button--primary" onClick={() => setOpen(true)}>
Open command palette
</button>
<CommandPalette
open={open}
onOpenChange={setOpen}
commands={[
{
id: 'new-guide',
label: 'New coaching guide',
shortcut: ['⌘', 'N'],
onSelect: () => {},
},
{ id: 'search-apps', label: 'Search apps', shortcut: ['⌘', 'K'], onSelect: () => {} },
{ id: 'contact', label: 'Open contact form', onSelect: () => {} },
{ id: 'publish', label: 'Publish site', shortcut: ['⌘', 'S'], onSelect: () => {} },
]}
/>
</div>
);
}When to use it
Reach for CommandPalette when there are enough actions or destinations that a keyboard-first
search beats hunting through menus — bound to a shortcut like ⌘K, it's there for the users
who'd rather never touch the mouse. If you only have three or four actions, a DropdownMenu
is less machinery for the same result.
It's built on Dialog, so opening it already blocks the rest of the page. It's not the right
container for something that should stay visible alongside what the reader is browsing — that's
Popover's job.
Binding a keyboard shortcut
The component only renders the palette — it doesn't own a global shortcut. Wire your own document-level listener to open it, same as most real command palettes.
Press ⌘K (or Ctrl+K) anywhere in this demo to open it.
Show code
import { useEffect, useState } from 'react';
import { CommandPalette } from '@elirobinson/react/components/organisms/CommandPalette';
export default function KeyboardShortcut() {
const [open, setOpen] = useState(false);
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
event.preventDefault();
setOpen(true);
}
}
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);
return (
<div>
<p>
Press <kbd>⌘K</kbd> (or <kbd>Ctrl</kbd>+<kbd>K</kbd>) anywhere in this demo to open it.
</p>
<CommandPalette
open={open}
onOpenChange={setOpen}
commands={[
{ id: 'new-recipe', label: 'New recipe', onSelect: () => {} },
{ id: 'new-lesson', label: 'New maths lesson', onSelect: () => {} },
{ id: 'invite-coach', label: 'Invite a coach', onSelect: () => {} },
]}
/>
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
commandsrequired | CommandPaletteCommand[] | — | — |
onOpenChangerequired | (open: boolean) => void | — | — |
openrequired | boolean | — | — |
className | string | — | — |
Accessibility
- Renders as a genuine modal
<dialog>viaDialog/DialogContent— not a positioned<div>imitating one. ADialogTitle("Command palette") is always rendered, wiring up the dialog's accessible name througharia-labelledby. - On open, focus moves to the search input automatically. It's the one place in this component where focus is managed explicitly, rather than left to the browser.
- Uses the same
aria-activedescendantpattern asCombobox(the two share a hook): DOM focus stays on the search input at all times, and the highlighted command is conveyed purely by id reference viaaria-activedescendant. - Keyboard:
ArrowDown/ArrowUpmove the highlight by one command, clamped at both ends (no wraparound).Enterruns the currently highlighted command — not just the first — and closes the palette.Escapecloses it without running anything. - Every re-open resets the search query and the highlighted command back to the top; nothing is sticky across opens.
- Filtering narrows the list live as you type; no matches renders a plain "No matching commands" row rather than an empty list.
- Keyboard-shortcut hints (
shortcut) render as real<kbd>elements next to each command. They're a visual hint only — the component doesn't bind the shortcut itself.
Do
- Wire your own document-level keydown listener (e.g. ⌘K) to open it.
- Give every command a stable, unique id and a working onSelect.
- Keep commands limited to what’s actually valid right now — it filters on every keystroke.
- Show the real shortcut as shortcut hints, and also bind it for real elsewhere.
Don't
- Nest another modal inside the palette — it’s already a native <dialog> with showModal(); stacking another on top fights the browser’s own modal handling.
- Rely on the shortcut hints to actually trigger the command — they’re display-only <kbd> tags.
- Pass a huge, unmemoized commands array on every render — it’s re-filtered on every keystroke.
- Use it as your only navigation — it’s a keyboard-first accelerator, not a replacement for a visible nav.