Skip to content

Components · Search and discovery

CommandPalette

A ⌘K / Ctrl+K palette: grouped commands, recent items, async search, nested pages and shortcut hints.

@rookcom/monarch-react/dataComponent

Commands, groups and shortcut hints

Press ⌘K or Ctrl+K anywhere on this page.

Usage examples

Recent items and async search

Nested page: Move to…

App shortcuts with useHotkey

Purpose

Lets keyboard users jump anywhere, run any action or find any record from one search field, without leaving the keyboard. Import from @rookcom/monarch-react/data.

Use when

  • An app-wide “go to / do / find” launcher opened with ⌘K or Ctrl+K.
  • A quick switcher over records the app searches on the server, grouped by type.
  • A short chain of choices from the keyboard, such as “Move to…” then a project (nested pages).

Avoid when

  • Picking one value for a form field: use Combobox.
  • Filtering the list on the page: use SearchInput or FilterBar.
  • The only way to reach an action: every command must also exist in the visible interface.

Design principles

  • The app owns the data. Commands, recent items and searches are passed in; Monarch owns keyboard, focus, filtering, debouncing, cancellation and announcements.
  • One field, one list: the input keeps focus and the active result moves with aria-activedescendant, so typing never stops.
  • Results are grouped by kind (“Actions”, “Go to”, “Documents”) with quiet headings; the active row is a muted fill, danger commands are red.
  • Shortcut hints sit at the end of each row in Kbd, so the palette teaches the shortcuts it lists.
  • Old results stay while new ones load, so the list does not flicker; a spinner replaces the search icon.

API reference

import { CommandPalette, defaultCommandFilter, formatHotkey, isTypingTarget, matchesHotkey, useHotkey } from '@rookcom/monarch-react/data';
ExportKindRenderingRoot class
CommandPalettecomponentclient.mn-command-palette-
defaultCommandFilterutilityclient–
formatHotkeyutilityclient–
isTypingTargetutilityclient–
matchesHotkeyutilityclient–
useHotkeyhookclient–

Built on Monarch Dialog (Radix Dialog): focus is trapped, the page behind is inert, and focus returns to the trigger on close. Without a trigger, focus returns to where it was.

items, recent and page items are CommandPaletteItems: { id, label, description?, icon?, group?, keywords?, shortcut?, page?, disabled?, tone?, keepOpen?, onSelect? }.

sources are CommandPaletteSources: { id, group, search(query, signal), minQueryLength?, debounceMs? }. Each source is debounced and aborted on its own.

pages maps a key to { title, placeholder?, items?, sources? }; an item with page opens it.

useHotkey(hotkey, handler, { enabled, inFields }), formatHotkey(hotkey), matchesHotkey(event, hotkey) and isTypingTarget(target) are exported for the app’s own shortcuts and shortcut hints.

defaultCommandFilter(item, query) is the default ranking, for apps that wrap it.

App-wide shortcut tables: compose useHotkey; a registry is not part of Monarch yet.

Generated from the TypeScript declarations of packages/react/src/components/search/command-palette.

Properties

CommandPalette

A ⌘K / Ctrl+K command palette: a modal dialog with a search combobox and a grouped listbox of commands, recent items and results from asynchronous sources, nested pages such as "Move to…", and shortcut hints. The app supplies every command and search; the palette owns keyboard, focus, filtering, debouncing, cancellation and announcements.

PropTypeDescription
classNamestringClass on the dialog content.
defaultOpenboolean
emptyStateReactNodeShown instead of `messages.empty` when nothing matches.
filter((item: CommandPaletteItem, query: string) => number)Ranks an item for the query: 0 hides it, higher sorts first within its group. Default: every word of the query must appear in the label, description or keywords; label prefix matches rank highest.
footerReactNodeContent at the bottom of the palette. Default: keyboard hints (↑↓ to navigate, ↵ to select, Esc to close). Pass `null` for none.
hotkeyfalse | HotkeyOpens and closes the palette from anywhere. Default `mod+k` (⌘K or Ctrl+K). Pass `false` to bind it yourself.
hotkeyInFieldsbooleanWhether a modified hotkey (⌘, Ctrl or Alt) also works while typing in another field. Default `true`. Single-key hotkeys such as `/` never fire while typing.
itemsreadonly CommandPaletteItem[]Commands shown on the first page. Filtered as the user types.
messagesPartial<CommandPaletteMessages>Override visible and announced strings.
onItemSelect((item: CommandPaletteItem, context: { page: string | null; }) => void)Called for every chosen item, after its own `onSelect`, with the page it was on.
onOpenChange((open: boolean) => void)
onSearchChange((search: string) => void)
openbooleanControlled open state.
pagesReadonly<Record<string, CommandPalettePage>>Nested pages by key, opened by items with `page`.
recentreadonly CommandPaletteItem[]Shown in a "Recent" group while the query is empty, on the first page. The app keeps this list (for example in localStorage) from `onItemSelect`.
searchstringControlled query.
sourcesreadonly CommandPaletteSource[]Asynchronous result sources for the first page.
triggerReactNodeAn element (usually a button) that opens the palette. Rendered with Radix `asChild`, so it must forward its ref and props to a DOM element, as Monarch Button does.

Variants

PropValuesDescription
item.tone'danger'A destructive command (data-tone="danger"): red text and a red-tinted active fill.
item.page'string'The item opens a nested page. A chevron marks it; the page title appears as a chip before the input.
footer'default hints''ReactNode''null'The bottom bar. Default: Kbd hints for ↑↓, ↵ and Esc. Hidden on phones when it only holds the default hints.

States

StateExposed asBehaviour
Active resultaria-selected="true", data-highlightedThe result Enter would run; muted fill (red tint for danger).
Disabled resultaria-disabled="true"Shown in the disabled colour, skipped by the arrow keys, not clickable.
Loading–A source is searching: the search icon becomes a spinner and “Searching…” is announced. Previous results stay.
No results–The query matches nothing: messages.empty or emptyState is shown and announced.
Source error–A source rejected: “Could not load documents.” with a Retry button, in the danger colour.
Nested pagedata-page="<key>"The page title chip shows before the input; the input is renamed “Command palette: <page>”.

Interaction behaviour

  • The hotkey (default mod+k: ⌘K on Apple platforms, Ctrl+K elsewhere) toggles the palette from anywhere. Pass an array for alternatives such as ["mod+k", "/"], or false to bind it yourself.
  • Single-key hotkeys such as / never fire while typing in a field or contenteditable; modified ones do unless hotkeyInFields={false}. Keys during IME composition are ignored.
  • Every open starts on the first page with an empty query. With an empty query the recent group comes first.
  • Typing filters items with the default ranking (every word must match label, description or keywords; label prefixes first) or your filter. Groups keep the order their first item appears in; ungrouped items come first.
  • Each source waits debounceMs (200) after the last keystroke, searches once the trimmed query is minQueryLength (1) long, and aborts its request when the query, page or open state changes.
  • Choosing an item runs its onSelect, then onItemSelect, then opens its page, or closes the palette unless keepOpen.
  • Pointer: moving over a row makes it active; clicking runs it. Rows never take focus.

Keyboard support

KeysAction
⌘K / Ctrl+K (configurable)Opens or closes the palette.
ArrowDown / ArrowUpMoves the active result, skipping disabled ones and wrapping at the ends.
PageDown / PageUpMoves the active result five rows, stopping at the ends.
EnterRuns the active result, or opens its nested page.
Backspace (empty query)Leaves the nested page.
EscapeLeaves the nested page; on the first page, closes the palette.
TabMoves between the input and any page chip or Retry button; focus stays inside the dialog.

Home and End stay with the input (caret to start or end), as in the APG combobox pattern.

Accessibility

Role
dialog containing combobox + listbox (option, group)
Accessible name
The dialog and input are named by messages.title (“Command palette”); the listbox by messages.results; each group by its heading.
  • APG combobox with listbox popup: the input has aria-expanded="true", aria-controls and aria-activedescendant; the active option has aria-selected="true".
  • Result count, “No results” and “Searching…” are announced through a polite live region after results settle.
  • Shortcut hints and icons are aria-hidden, so an option’s name is its label and description.
  • The default footer is aria-hidden: it repeats the keyboard model for sighted users only.
  • Verified with axe in jsdom (open, empty and nested-page states) and in Chromium, Firefox and WebKit on /components/command-palette.

Responsive behaviour

  • Up to 40rem wide, near the top of the viewport, capped at 34rem tall; the result list scrolls.
  • Below 640px it fills the screen (Dialog fullScreenOnPhone), keeping clear of notches, and hides shortcut hints and the default footer.
  • Rows grow to --mn-control-lg on coarse pointers so they are easy to tap.

Theme support

  • Monarch semantic tokens only: the raised surface and border of Dialog, --mn-color-bg-muted for the active row, --mn-color-danger-* for danger rows.
  • In forced-colours mode the active row gets a Highlight outline.
  • The entrance animation is removed under prefers-reduced-motion: reduce.

Do and don't

Do

  • Put every command in the visible interface too; the palette is a shortcut, not the only path.
  • Group results by kind and keep labels short verbs or nouns (“Create task”, “Settings”).
  • Abort your request with the given signal so stale responses never cost bandwidth.
  • Keep recent items yourself from onItemSelect, guarding storage access with try/catch.

Don't

  • Do not bind a single letter as the palette hotkey without checking it clashes with nothing; it already never fires while typing.
  • Do not render more than one palette with the same hotkey on a page.
  • Do not show shortcut hints for shortcuts the app does not actually bind.
  • Do not put product data or API calls in Monarch: pass them in as items and sources.
  • Combobox: Picks a value for a form field; the palette runs commands and navigates.
  • Dialog: The modal the palette is built on.
  • Kbd: Draws the shortcut hints and footer keys.
  • SearchInput: Filters the list on the current page instead.

Dependencies

KindDepends on
npm packagesreact
Monarch componentsDialog, Kbd, Spinner

Testing coverage

  • The hotkey opens it; dialog, combobox, listbox and named groups are wired (aria-controls, aria-activedescendant, aria-selected); axe in open, empty and nested-page states.
  • Arrow keys skip disabled results and wrap; Enter runs onSelect and onItemSelect and closes; PageUp/PageDown jump; keepOpen keeps it open; pointer hover moves the active row.
  • Filtering by label, description and keywords with ranking; empty state announced.
  • Recent group shows only with an empty query.
  • Shortcut hints render as Kbd and stay out of the option name; default footer hints.
  • Nested pages: placeholder, renamed input, back chip; Backspace and Escape leave the page before Escape closes; selections report their page.
  • Async sources: minimum length, one debounced request per settled query, stale results kept while loading, the previous request aborted on new input and on close, error with Retry.
  • Hotkey: single keys ignored in fields, modified keys work in fields unless hotkeyInFields={false}, hotkey={false} turns it off, it toggles closed.
  • Controlled open and search, a trigger, and a fresh query on reopen.
  • Helpers: formatHotkey per platform, matchesHotkey with symbols and Shift, isTypingTarget, defaultCommandFilter ranking and accent folding.
  • Renders on the server.
CheckCovered
Axe in jsdomYes
Server renderingYes
Browser suitesVisual baselines: /__gallery/command-palette, plus open-state shots at 1280 and 390; Cross-browser axe: /components/command-palette

Tests: packages/react/src/components/search/command-palette