Skip to content

Command

hc-command is a command palette, the shadcn Command / cmdk equivalent. It is the WAI-ARIA combobox pattern used as an action launcher: type to filter a grouped list, arrow to highlight, Enter to run. installCommand wires the filtering, aria-activedescendant keyboard navigation, the selection event, and an optional ⌘K opener.

Use it inside a native <dialog> for the classic centred palette — you get focus trapping, Escape-to-close, and a backdrop for free — or inline.

Navigation
Go homeG H
Open profileG P
Actions
New document⌘ N
Search files
<dialog class="hc-command-dialog" data-hotkey="k">
<div class="hc-command">
<input class="hc-command__input" type="text" role="combobox" autofocus
aria-label="Command menu" placeholder="Type a command…">
<div class="hc-command__list" role="listbox">
<div class="hc-command__group" role="group" aria-labelledby="g-nav">
<div class="hc-command__group-heading" id="g-nav">Navigation</div>
<div class="hc-command__item" role="option" data-value="home">
<span>Go home</span>
<kbd class="hc-command__shortcut">G H</kbd>
</div>
<div class="hc-command__item" role="option" data-value="profile">
<span>Open profile</span>
</div>
</div>
</div>
<div class="hc-command__empty" hidden>No results.</div>
</div>
</dialog>

The list uses role="listbox" with role="option" items grouped under role="group" headings (the cmdk / Radix structure). Each item’s data-value is what the select event reports and what the filter matches against (the .hc-command__shortcut text is excluded from the match).

import { installCommand } from '@hypermedia-components/core';
installCommand(); // idempotent; returns an uninstaller

The zero-config @hypermedia-components/core/behaviors entry installs it automatically.

Put data-hotkey="k" on the <dialog> (any single key; default k) and the behavior toggles it with /Ctrl

  • the key, focusing the input and resetting the filter on open:
<dialog class="hc-command-dialog" data-hotkey="k"></dialog>

The handler calls preventDefault() so the browser’s own Ctrl+K shortcut does not also fire. You can still open the dialog from a button with dialog.showModal()autofocus on the input puts the cursor in the search box.

Typing fuzzy-filters and re-ranks the items: the query characters must appear in order (a subsequence), and a match scores higher when its characters are contiguous or land on a word / camelCase boundary. Items reorder by score — the best match floats to the top, even across groups, and ties keep the authored order. Clearing the query restores the original sequence. Filtering is always client-side; the palette never touches the network.

To keep the previous plain behaviour — a case-insensitive substring match with no reordering — set data-filter="substring" on the .hc-command:

<div class="hc-command" data-filter="substring"></div>

The .hc-command__shortcut text is excluded from the match (only the item’s label is searched).

KeyAction
TypeFuzzy-filter and re-rank items; empty groups and their headings hide; an empty state shows when nothing matches.
/ Move the highlight (wraps; skips disabled items).
Home / EndFirst / last visible item.
EnterRun the highlighted item.
EscapeClose (native <dialog>).

DOM focus stays on the input; the highlighted item is tracked with aria-activedescendant, per the WAI-ARIA combobox pattern.

Running an item dispatches a bubbling hc:commandselect on the .hc-command root and (when inside a <dialog>) closes it:

command.addEventListener('hc:commandselect', (e) => {
const { item, value } = e.detail; // value = item's data-value
routeTo(value);
});

Run an action server-side straight off the event:

<div class="hc-command"
data-hx-post="/commands/run"
data-hx-trigger="hc:commandselect"
data-hx-vals='js:{ command: event.detail.value }'>
</div>

Run the chosen command inline instead of wiring JS:

<div class="hc-command"
_="on hc:commandselect call runCommand(event.detail.value)">
</div>

More patterns: Hyperscript → Reacting to component events.

  • The input is role="combobox" with aria-expanded, aria-controls, and aria-activedescendant; the list is role="listbox", groups are role="group" with aria-labelledby headings, items are role="option".
  • Inside a <dialog> opened with showModal(), focus is trapped and Escape closes — both native. Keep autofocus on the input so the search field is ready on open.
  • Always give the input an accessible name (aria-label or a label).
  • Disabled items (aria-disabled="true") are skipped by keyboard and ignored on click.
Token pathPurpose
command.bg / fg / border / radiusPalette surface.
command.input-* / placeholder-fgSearch row.
command.list-max-height / list-padding-blockScrollable list.
command.heading-*Group headings.
command.item-*Item rows, including the item-active-bg highlight (tracks data-color).
command.shortcut-*Shortcut chips.
command.empty-*Empty state.
command.dialog-width / dialog-offset / backdropThe .hc-command-dialog wrapper.
Show the generated CSS variables
--hc-command-bg | -fg | -border | -radius
--hc-command-input-height | -input-padding-x | -input-font-size | -placeholder-fg
--hc-command-list-padding-block | -list-max-height
--hc-command-heading-fg | -heading-font-size | -heading-font-weight | -heading-padding-y
--hc-command-item-padding-x | -item-padding-y | -item-gap | -item-font-size
--hc-command-item-fg | -item-hover-bg | -item-active-bg | -item-active-fg | -item-disabled-fg
--hc-command-shortcut-fg | -shortcut-font-size | -shortcut-padding-x
--hc-command-empty-fg | -empty-padding-y
--hc-command-dialog-width | -dialog-offset | -backdrop
  • Combobox — the same type-to-filter pattern bound to a single form value.
  • Menu — a dropdown action menu from a trigger button.
  • Dialog — the native <dialog> surface the palette sits in.