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.
<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.
<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.
<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.
<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.
<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.
<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.
<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
<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
| Attribute | Type | Default | Description |
|---|---|---|---|
open | boolean | N/A | Controlled visibility |
default-open | boolean | false | Initial visibility when open is not provided |
label | string | 'Command palette' | Accessible label for the dialog |
placeholder | string | 'Type a command or search…' | Placeholder text for the search input |
shortcut | string | 'mod+k' | Global keyboard shortcut (keymap syntax) that toggles the palette; '' disables it |
no-filter | boolean | false | Disable built-in client-side filtering (for server-driven search) |
loading | boolean | false | Shows a loading indicator next to the search input |
empty-text | string | 'No results found.' | Message shown when no item matches the query |
keep-open-on-select | boolean | false | Keep the palette open after an item is selected, refocusing the search input |
ore-command-palette Properties
| Property | Type | Description |
|---|---|---|
items | CommandPaletteItemInput[] | JS-driven command list: takes precedence over slotted ore-command-palette-item elements |
ore-command-palette Slots
| Slot | Description |
|---|---|
| (default) | ore-command-palette-item elements (alternative/supplement to items) |
preview | Optional detail pane rendered beside the list while the slot has content; pair with active-change |
ore-command-palette-item Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | '' | Value emitted by the select event and matched by search |
label | string | N/A | Explicit label text; falls back to the element's text content |
group | string | N/A | Group heading the item is clustered under |
icon | string | N/A | Lucide icon name rendered at the start of the row |
shortcut | string | N/A | Display-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") |
keywords | string | N/A | Comma-separated extra search terms matched in addition to the label |
disabled | boolean | false | Excludes the item from keyboard navigation and selection |
Events
| Event | Detail | Description |
|---|---|---|
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
| Property | Description | Default |
|---|---|---|
--command-palette-bg | Panel background color | var(--color-canvas) |
--command-palette-border-color | Panel border color | Theme-dependent |
--command-palette-radius | Panel border radius | var(--rounded-lg) |
--command-palette-shadow | Panel drop shadow | var(--shadow-2xl) |
--command-palette-max-width | Maximum panel width | 40rem |
--command-palette-backdrop | Backdrop overlay color | Theme-dependent |
--command-palette-option-hover-bg | Item background on hover | Theme-dependent |
--command-palette-option-focus-bg | Item background when keyboard-focused | Theme-dependent |
--command-palette-preview-width | Preview 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.