Skip to content

Command Palette

A searchable, keyboard-driven list of commands presented in a centered modal — the "⌘K" pattern popularized by editors and productivity apps. Built on the native <dialog> element for focus trapping, top-layer stacking, and Escape-to-close, and on @vielzeug/keymap for the global shortcut that opens it from anywhere on the page.

This is a centered, global modal — not an anchored, inline dropdown. For a Slack/Notion-style /-triggered menu that pops up under the caret inside a ore-message-composer, reach for ore-combobox or ore-menu instead; keep ore-command-palette for a single, page-wide "jump to action" shortcut.

Basic Usage

Declare commands with ore-command-palette-item elements in the default slot. Each item needs a value; the visible label falls back to the element's text content when label is omitted.

PreviewCode
RTL
html
<ore-button id="open-btn">Open (⌘K / Ctrl+K)</ore-button>

<ore-command-palette id="palette" label="Command palette">
  <ore-command-palette-item value="new-file" group="File" shortcut="⌘+N">New File</ore-command-palette-item>
  <ore-command-palette-item value="open-file" group="File" shortcut="⌘+O">Open File…</ore-command-palette-item>
  <ore-command-palette-item value="save-file" group="File" shortcut="⌘+S">Save</ore-command-palette-item>
  <ore-command-palette-item value="toggle-theme" group="View" keywords="dark,light,appearance"
    >Toggle Theme</ore-command-palette-item
  >
  <ore-command-palette-item value="open-settings" group="View">Open Settings</ore-command-palette-item>
</ore-command-palette>

<script type="module">
  import '@vielzeug/refine/command-palette';
  import '@vielzeug/refine/button';

  const palette = document.getElementById('palette');

  document.getElementById('open-btn').addEventListener('click', () => {
    palette.setAttribute('open', '');
  });

  palette.addEventListener('select', (e) => {
    console.log('command selected:', e.detail.value);
  });
</script>

The global shortcut (mod+k by default — ⌘K on macOS, Ctrl+K elsewhere) works as soon as the element is connected, independent of the open attribute; pressing it again while open closes the palette.

Data-Driven Items

Pass the items property for programmatically generated commands instead of (or in addition to) slotted ore-command-palette-item elements. The items property always takes precedence over slotted items when both are present.

html
<ore-command-palette id="palette" label="Jump to…"></ore-command-palette>

<script type="module">
  import '@vielzeug/refine/command-palette';

  document.getElementById('palette').items = [
    { value: 'dashboard', label: 'Go to Dashboard', group: 'Navigate' },
    { value: 'billing', label: 'Go to Billing', group: 'Navigate' },
    { value: 'invite', label: 'Invite Teammate', group: 'Actions', shortcut: '⌘+I' },
    { value: 'logout', label: 'Log Out', group: 'Actions', disabled: true },
  ];
</script>

Groups and Icons

Items sharing a group value are clustered under a heading, in the order groups first appear. Add an icon (a Lucide icon name) for a leading icon.

PreviewCode
RTL
html
<ore-command-palette id="grouped-palette" label="Command palette" open>
  <ore-command-palette-item value="new-file" group="File" icon="file-plus" shortcut="⌘+N"
    >New File</ore-command-palette-item
  >
  <ore-command-palette-item value="open-file" group="File" icon="folder-open" shortcut="⌘+O"
    >Open File…</ore-command-palette-item
  >
  <ore-command-palette-item value="find" group="Edit" icon="search" shortcut="⌘+F">Find</ore-command-palette-item>
  <ore-command-palette-item value="replace" group="Edit" icon="replace">Replace</ore-command-palette-item>
</ore-command-palette>

<script type="module">
  import '@vielzeug/refine/command-palette';
</script>

Disabled Items

Set disabled on an item to keep it visible but skip it during keyboard navigation, typeahead-style filtering, and click selection.

html
<ore-command-palette label="Command palette" open>
  <ore-command-palette-item value="export">Export Project</ore-command-palette-item>
  <ore-command-palette-item value="delete" disabled>Delete Project (no permission)</ore-command-palette-item>
</ore-command-palette>

Keeping the Palette Open

By default, selecting an item closes the palette. Set keep-open-on-select for commands the user might invoke repeatedly (e.g. inserting several snippets in a row) — the search input is refocused after each selection.

html
<ore-command-palette label="Insert snippet" keep-open-on-select open>
  <ore-command-palette-item value="snippet-1">Insert "Hello world"</ore-command-palette-item>
  <ore-command-palette-item value="snippet-2">Insert date stamp</ore-command-palette-item>
</ore-command-palette>

Custom Shortcut, Filtering, and Empty State

Override the global shortcut with shortcut, or set it to an empty string to disable the built-in trigger entirely and open the palette from your own UI instead. Use no-filter when items is already filtered server-side, and empty-text to customize the no-results message.

html
<ore-command-palette id="search-palette" label="Search" shortcut="mod+shift+p" empty-text="No commands match." loading>
</ore-command-palette>

<script type="module">
  import '@vielzeug/refine/command-palette';

  const palette = document.getElementById('search-palette');

  palette.addEventListener('search', async (e) => {
    palette.setAttribute('loading', '');
    palette.items = await fetchResults(e.detail.query);
    palette.removeAttribute('loading');
  });
</script>

Set no-filter on this pattern once results come back already scoped to the query — otherwise the built-in client-side filter runs again on top of the server response.

Listening to Events

html
<ore-command-palette id="palette" label="Command palette">
  <ore-command-palette-item value="new-file">New File</ore-command-palette-item>
  <ore-command-palette-item value="open-file">Open File…</ore-command-palette-item>
</ore-command-palette>

<script type="module">
  import '@vielzeug/refine/command-palette';

  const palette = document.getElementById('palette');

  // Fired when a command is chosen (click or Enter)
  palette.addEventListener('select', (e) => {
    console.log('selected:', e.detail.value, e.detail.label);
  });

  // Fired on every keystroke in the search input
  palette.addEventListener('search', (e) => {
    console.log('query:', e.detail.query);
  });

  palette.addEventListener('open', (e) => {
    console.log('opened via:', e.detail.reason); // 'keyboard', 'programmatic', or 'trigger'
  });

  palette.addEventListener('close', (e) => {
    console.log('closed via:', e.detail.reason); // 'escape', 'outsideClick', 'programmatic', or 'trigger'
  });
</script>

Use value in a single select listener to dispatch commands rather than wiring per-item click handlers.

API Reference

ore-command-palette Attributes

AttributeTypeDefaultDescription
openbooleanfalseControls whether the palette is visible
labelstring'Command palette'Accessible label for the dialog
placeholderstring'Type a command or search…'Placeholder text for the search input
shortcutstring'mod+k'Global keyboard shortcut (keymap syntax) that toggles the palette; '' disables it
no-filterbooleanfalseDisable built-in client-side filtering (for server-driven search)
loadingbooleanfalseShows a loading indicator next to the search input
empty-textstring'No results found.'Message shown when no item matches the query
keep-open-on-selectbooleanfalseKeep the palette open after an item is selected, refocusing the search input

ore-command-palette Properties

PropertyTypeDescription
itemsCommandPaletteItemInput[]JS-driven command list — takes precedence over slotted ore-command-palette-item elements

ore-command-palette Slots

SlotDescription
(default)ore-command-palette-item elements (alternative/supplement to items)

ore-command-palette-item Attributes

AttributeTypeDefaultDescription
valuestring''Value emitted by the select event and matched by search
labelstringExplicit label text; falls back to the element's text content
groupstringGroup heading the item is clustered under
iconstringLucide icon name rendered at the start of the row
shortcutstringDisplay-only keyboard hint rendered at the end of the row — one <kbd> per key, separated with + (e.g. "⌘+S"). A literal + key isn't representable this way — spell it out (e.g. "Ctrl+Plus")
keywordsstringComma-separated extra search terms matched in addition to the label
disabledbooleanfalseExcludes the item from keyboard navigation and selection

Events

EventDetailDescription
select{ value: string, label: string, item: CommandPaletteItem }Emitted when a command is chosen (click or Enter)
search{ query: string }Emitted on every keystroke in the search input
open{ reason: 'keyboard' | 'programmatic' | 'trigger' }Emitted when the palette opens
close{ reason: 'escape' | 'outsideClick' | 'programmatic' | 'trigger' }Emitted when the palette closes

CSS Custom Properties

PropertyDescriptionDefault
--command-palette-bgPanel background colorvar(--color-canvas)
--command-palette-border-colorPanel border colorTheme-dependent
--command-palette-radiusPanel border radiusvar(--rounded-lg)
--command-palette-shadowPanel drop shadowvar(--shadow-2xl)
--command-palette-max-widthMaximum panel width40rem
--command-palette-backdropBackdrop overlay colorTheme-dependent
--command-palette-option-hover-bgItem background on hoverTheme-dependent
--command-palette-option-focus-bgItem background when keyboard-focusedTheme-dependent

Accessibility

The dialog panel follows the WAI-ARIA Dialog (Modal) Pattern, built on the native <dialog> element: Tab/Shift+Tab trap focus inside the panel, Escape closes it, and focus returns to the triggering element on close. The label attribute becomes the dialog's aria-label — always set one, since the palette has no visible title.

The search input follows the collapsible listbox pattern: it carries role="combobox", aria-expanded="true", aria-controls pointing at the listbox, and a reactive aria-activedescendant that tracks the keyboard-focused row. The listbox itself has role="listbox"; each command row has role="option" with aria-selected reflecting keyboard focus and aria-disabled for disabled items. Group headings are role="presentation" decorative text, not part of the accessibility tree's option count.

ArrowUp/ArrowDown move focus through enabled items (wrapping at the ends); Home/End jump to the first/last enabled item; Enter selects the focused item — or the first result if none has been focused yet, so pressing Enter immediately after typing a query works as expected. Disabled items are skipped entirely by keyboard navigation. On open, the search input receives focus automatically; on close, the query resets so the next open starts from a clean slate.

Because the palette can be summoned from anywhere via its global keyboard shortcut, avoid relying on shortcut collisions with other page-level bindings — check for conflicts with findShortcutConflicts if your app registers other global shortcuts.