Command palette
A command palette is a modal launcher (⌘K) — a search input over a filterable,
groupable list of commands and links. The modal shell is a native <dialog>
driven by the existing Dialog behavior
(open/close/focus-trap/backdrop/Escape are all native); enhanceCommandPalette
adds the combobox/listbox ARIA, type-to-filter, arrow navigation, and an
optional global hotkey.
import { enhanceCommandPalette } from "@relements/core/behaviors/command-palette";enhanceCommandPalette(document);Open it with a data-re-dialog-trigger button or the data-re-command-hotkey
shortcut (e.g. "mod+k" — mod is ⌘ on macOS, Ctrl elsewhere). The hotkey
claims the combo: it stops the keystroke from bubbling to page-level handlers,
so a site-wide ⌘K search won’t also fire. (On this docs page, that’s why ⌘K opens
the demo rather than the site search — use / for search here.)
<button
type="button"
class="re-button"
data-variant="secondary"
data-re-dialog-trigger
data-re-dialog-target="cmdk"
>
Search… <kbd class="re-kbd">⌘K</kbd>
</button>
<dialog
id="cmdk"
class="re-dialog re-command-palette"
aria-label="Command palette"
data-re-dialog-close-on-backdrop
data-re-command-palette
data-re-command-hotkey="mod+k"
>
<form class="re-command-palette__search" role="search" method="dialog">
<svg
class="re-command-palette__search-icon"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
focusable="false"
>
<circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" />
<path d="m20 20-3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
<input
type="search"
class="re-command-palette__input"
autocomplete="off"
autofocus
placeholder="Type a command or search…"
aria-label="Search commands"
/>
<button type="submit" class="re-sr-only" data-re-dialog-close>Close</button>
</form>
<ul class="re-command-palette__list">
<li class="re-command-palette__group">
<span class="re-command-palette__group-label">Navigation</span>
<ul>
<li class="re-command-palette__item">
<a href="#dashboard" class="re-command-palette__action">
<span class="re-command-palette__item-label">Go to Dashboard</span>
<kbd class="re-kbd re-command-palette__item-hint">G D</kbd>
</a>
</li>
<li class="re-command-palette__item">
<a href="#settings" class="re-command-palette__action">
<span class="re-command-palette__item-label">Go to Settings</span>
<kbd class="re-kbd re-command-palette__item-hint">G S</kbd>
</a>
</li>
</ul>
</li>
<li class="re-command-palette__group">
<span class="re-command-palette__group-label">Actions</span>
<ul>
<li class="re-command-palette__item">
<span class="re-command-palette__action" data-command="new-doc">
<span class="re-command-palette__item-label">Create document</span>
</span>
</li>
<li class="re-command-palette__item">
<span class="re-command-palette__action" data-command="invite">
<span class="re-command-palette__item-label">Invite teammate</span>
</span>
</li>
<li class="re-command-palette__item" aria-disabled="true">
<span class="re-command-palette__action">
<span class="re-command-palette__item-label">Delete workspace</span>
</span>
</li>
</ul>
</li>
</ul>
<div class="re-command-palette__empty" hidden>
<div class="re-empty-state" data-size="sm" role="status">
<p class="re-empty-state__title">No results</p>
<p class="re-empty-state__description">Try a different search.</p>
</div>
</div>
<footer class="re-command-palette__footer" aria-hidden="true">
<span><kbd class="re-kbd">↑</kbd><kbd class="re-kbd">↓</kbd> navigate</span>
<span><kbd class="re-kbd">↵</kbd> select</span>
<span><kbd class="re-kbd">esc</kbd> close</span>
</footer>
</dialog> - No-JS baseline: the static markup is a plain
<input type="search">over a list of real links/<span>rows — norole=combobox/listbox/optionis shipped until JS can back it.enhanceCommandPaletteadds the widget ARIA on enhance. - Link rows are real
<a href>(navigable without JS); on enhance the href is lifted to data and the row becomes a non-interactive option (so the listbox has no nested interactive controls). Command rows are<span data-command>. - Activation dispatches
re-command(bubbles, cancelable;detail = { command, href, option }); if not prevented and the row is a link, it navigates. - Focus stays in the input (aria-activedescendant). On close the query resets;
Escape and the backdrop are handled by the dialog. Always keep palettes
dismissible — never add
data-re-dialog-no-dismiss.
Accessibility
Section titled “Accessibility”- Keyboard — type to filter (substring match on each row’s label). ↓ / ↑
move the active option, wrapping at the ends and skipping hidden or
aria-disabledrows. Enter activates the active option (dispatchesre-command, then navigates if it’s a link). Escape and the backdrop close the dialog (native). The optionaldata-re-command-hotkey(e.g."mod+k") opens it from anywhere — see above. Focus never leaves the input, so there is no Tab-through-rows; rows are non-interactive options, activated by click or Enter. - Focus — the dialog opens with native
showModal(), which traps focus and makes the background inert; closing returns focus to the trigger. The search input keeps focus the whole time and opts out of the:focus-visiblering on purpose (the open modal and caret already mark it, and a ring would hug the borderless field). The active row is indicated by abg-mutedhighlight viaaria-activedescendantrather than real focus. - Semantics — on enhance the input becomes a
role="combobox"(aria-autocomplete="list",aria-controlsthe list,aria-expandedreflecting whether any rows match, andaria-activedescendantpointing at the highlighted row); the list becomesrole="listbox", each group a labelledrole="group", and each row arole="option"togglingaria-selected. The empty-state carriesrole="status"and rewrites its text on the transition to zero results, so the change is announced. - Notes — none of that ARIA ships in the static markup; the no-JS baseline is
a plain search field over real links/
<span>rows (see Notes above). The keyboard-hint footer isaria-hidden, the search icon is decorative (aria-hidden), and the native close control is.re-sr-only. In forced-colors mode the active option is painted with the systemHighlightcolor so keyboard navigation stays visible once the muted background flattens.
See the dialog component for the underlying modal behavior and the accessibility guide for project-wide conventions.