ComponentStandalone

Command Palette

⌘K-style centred modal with a search input and filtered list of actions. Items group automatically, support icons, descriptions, and shortcut hints. Bind a global hotkey (default mod+k) or open it programmatically.

Installation

import { WrCommandPalette, type WrCommandItem } from 'ngwr/command-palette';

@Component({ imports: [WrCommandPalette] })
export class AppShell {
  protected readonly commands: WrCommandItem[] = [
    { id: 'theme.light', label: 'Switch to light theme', group: 'Theme', shortcut: 'T L' },
    { id: 'theme.dark',  label: 'Switch to dark theme',  group: 'Theme', shortcut: 'T D' },
    { id: 'docs.search', label: 'Search docs',           group: 'Docs',  shortcut: '/' },
  ];

  protected onPicked(item: WrCommandItem) {
    console.log('picked', item.id);
  }
}

Basic usage

Drop a single palette at your app shell. The default hotkey mod+k (⌘K on macOS, Ctrl+K elsewhere) opens it. The (picked) output fires after the item's own action() callback if it has one.

<!-- Drop at the root once; opens via global hotkey (default: mod+k). -->
<wr-command-palette
  [items]="commands"
  trigger="mod+k"
  (picked)="onPicked($event)"
/>

Live demo

Click the button or press ⌘K / Ctrl+K. Type to filter. ↑/↓ to move, Enter to pick, Esc to close.

Nothing picked yet.

Responsive (full-screen)

With responsive — or app-wide via provideWrResponsiveOverlays() — the palette fills the screen from the top edge on small viewports instead of a centred modal (it auto-focuses its input, so it stays clear of the on-screen keyboard). Open this on a phone (or narrow the window below 640px).

<wr-command-palette responsive [items]="items" [(open)]="open" />

Programmatic control

Set [(open)] and pass [trigger] of null to disable the auto-bound hotkey when you want to open it from your own UI.

<button (click)="open.set(true)">Open palette</button>

<wr-command-palette
  [items]="commands"
  [(open)]="open"
  [trigger]="null"
  (picked)="onPicked($event)"
/>

Backed by a server

[items] does not have to be a fixed list. Bind [(query)] to read what the user typed, dispatch on (searchChange) — which fires once the query settles, not once per keystroke — and feed the answer back into [items]. Add serverSearch so the built-in substring filter steps aside: a backend that ranks or tolerates typos returns rows whose labels do not contain the query at all, and filtering them again on the client would throw away the good ones. [loading] swaps the empty row for “Searching…”, because a palette that reports “No results” between every keystroke and its answer is stating something that is not true yet.

<wr-command-palette
  [items]="results()"
  [(query)]="query"
  [loading]="loading()"
  serverSearch
  [debounceMs]="250"
  (searchChange)="search($event)"
/>

API

NameDescriptionTypeDefault
paletteLabelAccessible name. Falls back to commandPalette.label, then 'Command palette'.string | nullnull
itemsItems shown in the palette.readonly WrCommandItem[][]
openControlled open state (two-way bindable).booleanfalse
triggerGlobal hotkey that opens the palette. null disables auto-binding.WrHotkeySpec | null'mod+k'
placeholderSearch input placeholder. Falls back to commandPalette.placeholder.string | nullnull
emptyTextText shown when no items match. Falls back to commandPalette.noResults.string | nullnull
loadingTextOverrides the commandPalette.loading catalog string, shown instead of {@link emptyText} while {@link loading} is true.string | nullnull
closeOnPickAuto-close on (picked). Set false to keep open.booleantrue
responsivePresent the palette full-screen on small viewports instead of a centred modal. undefined follows the app-wide provideWrResponsiveOverlays() setting; true/false overrides it. The palette docks to the top (it auto-focuses its input, so the on-screen keyboard stays clear).boolean | undefinedundefined
(picked)Fires when the user commits an item (Enter / click).WrCommandItem
queryThe search text. Two-way, so a host can seed it, clear it, or read what the user is looking for — the palette used to keep this to itself, which is what made it impossible to back with anything but a static items array. Writing it does NOT re-run a host's search on its own: the debounced {@link searchChange} output is what fires, on exactly the cadence {@link debounceMs} sets.string''
serverSearchThe item list is already scoped to the query upstream — skip the built-in client-side filter. Set it when [items] is fed from a server via the {@link searchChange} output. Without this, a backend that ranks or tolerates typos (returning rows whose labels do not literally contain the query) would have those rows hidden again on the client — the same rule wr-select follows, and the same reason.booleanfalse
loadingA host-driven search is in flight. Swaps the empty row for {@link loadingText} — an async palette that says "No results" between every keystroke and its answer is stating something false.booleanfalse
debounceMsHow long the query must settle before {@link searchChange} fires, in ms. A search request per keystroke is what exhausts a metered search backend: typing select is six requests at 0 and roughly one at 250.number250
(searchChange)The settled query. This is the hook for a server-backed palette: dispatch on it, then feed the result into [items] with [serverSearch] set. Fires on the {@link debounceMs} cadence, NOT per keystroke — queryChange is the per-keystroke signal if that is what you want.string

Types

NameDescriptionTypeDefault
WrCommandItem{ id, label, description?, group?, icon?, keywords?, shortcut?, action? }interface