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. Shortcut hints use ore-keyboard-key, which is also available for settings screens and key-state displays. Use open as the controlled state and update it from open-change; use default-open only to initialize an uncontrolled 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.

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

<ore-command-palette id="palette" label="Jump to…"></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.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 registered 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.

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="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>

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

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

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.

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

<ore-command-palette id="palette" label="Insert snippet" keep-open-on-select>
  <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>

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

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

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.

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

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

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

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

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

  palette.addEventListener('search', async (e) => {
    palette.setAttribute('loading', '');
    // Simulate async fetch
    await new Promise(r => setTimeout(r, 500));
    palette.items = [
      { value: 'cmd-1', label: 'Sample result for: ' + e.detail.query },
      { value: 'cmd-2', label: 'Another match for: ' + 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.

Previewing the Active Row ​

Put content in the preview slot to render a detail pane beside the command list: the pane opens automatically while the slot has content and folds away when the slot is empty, so a palette that never uses it keeps the plain single-column layout. Listen to active-change to keep the pane in step with the row the user points at: it fires whenever the focused row changes (keyboard, hover, typing, open, close), with item: undefined when no row is active. On narrow screens the pane drops below the list instead of sitting beside it.

PreviewCode
RTL
html
<ore-command-palette id="preview-palette" label="Command palette" open>
  <ore-command-palette-item value="keyword-focus" group="Keywords" icon="eye">Focus</ore-command-palette-item>
  <ore-command-palette-item value="keyword-range" group="Keywords" icon="ruler">Range</ore-command-palette-item>
  <ore-command-palette-item value="keyword-cover" group="Keywords" icon="shield">Cover</ore-command-palette-item>
  <div slot="preview" id="preview-pane" style="display: flex; flex-direction: column; gap: 0.5rem">
    <strong id="preview-title"></strong>
    <span id="preview-body" style="color: var(--color-contrast-600)"></span>
  </div>
</ore-command-palette>

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

  const entries = {
    'keyword-focus': 'A hunter may spend an action to steady themselves, drawing the card that answers what comes next.',
    'keyword-range': 'Measured in sectors from the attacker: a target outside the printed range cannot be attacked.',
    'keyword-cover': 'Figures behind cover are harder to hit: the attacker must spend extra range to aim around it.',
  };

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

  palette.addEventListener('active-change', (e) => {
    const text = entries[e.detail.value];
    document.getElementById('preview-title').textContent = e.detail.label;
    document.getElementById('preview-body').textContent = text ?? 'No entry for this command.';
  });
</script>

The slot is plain light DOM: the palette only owns where and when it renders, so the pane can hold any markup or components the host app already uses. Pair it with no-filter when the catalogue is app-ranked: the preview then explains the app's own ranking as the user arrows through the hits.

Listening to Events ​

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">New File</ore-command-palette-item>
  <ore-command-palette-item value="open-file">Open File…</ore-command-palette-item>
</ore-command-palette>

<ore-box id="log" style="margin-top: 1rem; font-family: monospace; font-size: 0.875rem; max-height: 200px; overflow-y: auto;"></ore-box>

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

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

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

  palette.addEventListener('select', (e) => {
    const msg = `Selected: ${e.detail.value} (${e.detail.label})`;
    console.log(msg);
    log.innerHTML += msg + '<br/>';
  });

  palette.addEventListener('search', (e) => {
    console.log('search:', e.detail.query);
  });

  palette.addEventListener('open-change', (e) => {
    console.log('open:', e.detail.open);
  });
</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
openbooleanN/AControlled visibility
default-openbooleanfalseInitial visibility when open is not provided
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)
previewOptional detail pane rendered beside the list while the slot has content; pair with active-change

ore-command-palette-item Attributes

AttributeTypeDefaultDescription
valuestring''Value emitted by the select event and matched by search
labelstringN/AExplicit label text; falls back to the element's text content
groupstringN/AGroup heading the item is clustered under
iconstringN/ALucide icon name rendered at the start of the row
shortcutstringN/ADisplay-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")
keywordsstringN/AComma-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)
active-change{ value: string, label: string, item?: CommandPaletteItem }Emitted when the focused row changes; item is undefined when no row is active
search{ query: string }Emitted on every keystroke in the search input
open-change{ open: boolean, reason: string }Emitted whenever the palette opens or 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
--command-palette-preview-widthPreview pane width (when the preview slot has content)18rem

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.