CommandPalette

Dialog-based command list with search filtering and keyboard-shortcut hints.

organismsSource
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

PropTypeDefaultDescription
commandsrequiredCommandPaletteCommand[]
onOpenChangerequired(open: boolean) => void
openrequiredboolean
classNamestring

Accessibility

  • Renders as a genuine modal <dialog> via Dialog/DialogContent — not a positioned <div> imitating one. A DialogTitle ("Command palette") is always rendered, wiring up the dialog's accessible name through aria-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-activedescendant pattern as Combobox (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 via aria-activedescendant.
  • Keyboard: ArrowDown/ArrowUp move the highlight by one command, clamped at both ends (no wraparound). Enter runs the currently highlighted command — not just the first — and closes the palette. Escape closes 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.