Skip to content

Command

A command palette or search-driven menu that filters, ranks, and reorders items as the user types. It supports grouped items, keyboard shortcuts, vim-style bindings, pointer-free keyboard navigation, flexible ranking via keywords, and keeps the original authored DOM order when no search is active.

import { Command } from "@areia/slots";

document.querySelector("#root")!.innerHTML = `
  <div data-slot="command" data-label="Command Menu" class="win95-panel w-80 p-2">
    <div data-slot="command-input-wrapper" class="mb-2">
      <input
        data-slot="command-input"
        id="command-input"
        placeholder="Search..."
        class="win95-input w-full"
      />
    </div>
    <div data-slot="command-list" class="win95-inset p-3">
      <div data-slot="command-empty" hidden class="px-3 py-1 text-neutral-600">
        No results found.
      </div>
      <div data-slot="command-group">
        <div data-slot="command-group-heading" class="win95-label">
          Actions
        </div>
        <div
          data-slot="command-item"
          data-value="copy"
          data-keywords="duplicate"
          class="win95-menu-item"
        >
          Copy
          <span
            data-slot="command-shortcut"
            class="float-right ml-8 text-neutral-700"
          >
            ⌘C
          </span>
        </div>
        <div
          data-slot="command-item"
          data-value="paste"
          data-keywords="insert"
          class="win95-menu-item"
        >
          Paste
          <span
            data-slot="command-shortcut"
            class="float-right ml-8 text-neutral-700"
          >
            ⌘V
          </span>
        </div>
      </div>
      <div data-slot="command-separator" class="win95-separator"></div>
      <div data-slot="command-group">
        <div data-slot="command-group-heading" class="win95-label">
          Navigation
        </div>
        <div data-slot="command-item" data-value="home" class="win95-menu-item">
          Home
          <span
            data-slot="command-shortcut"
            class="float-right ml-8 text-neutral-700"
          >
            ⌘H
          </span>
        </div>
        <div
          data-slot="command-item"
          data-value="settings"
          class="win95-menu-item"
        >
          Settings
          <span
            data-slot="command-shortcut"
            class="float-right ml-8 text-neutral-700"
          >
            ⌘,
          </span>
        </div>
      </div>
      <div data-slot="command-item" data-disabled class="win95-menu-item">
        Disabled item
      </div>
    </div>
  </div>
`;

const root = document.querySelector('[data-slot="command"]')!;
const controller = Command.createCommand(root, {
  label: "Command Menu",
  loop: true,
  vimBindings: true,
});

Import

import { Command } from "@areia/slots";

Usage

import { Command } from "@areia/slots";

// Auto-bind a single root
const root = document.querySelector('[data-slot="command"]');
const controller = Command.createCommand(root, {
  label: "Command Menu",
  onValueChange: (value) => console.log("active item:", value),
  onSelect: (value) => console.log("selected:", value),
});

// Auto-bind all unbound roots in scope
const controllers = Command.create();

Expected Markup

The command expects a search input and a list of items. Items can be organized into groups with headings. Use data-value for deterministic item identification, data-keywords to boost ranking, data-disabled to disable items, and data-force-mount to keep filtered-out items in the DOM.

<label for="command-input">Actions</label>
<div data-slot="command" data-label="Command Menu">
  <div data-slot="command-input-wrapper">
    <input
      data-slot="command-input"
      id="command-input"
      placeholder="Search..."
    />
  </div>
  <div data-slot="command-list">
    <div data-slot="command-empty" hidden>
      No results found.
    </div>

    <div data-slot="command-group">
      <div data-slot="command-group-heading">Actions</div>
      <div
        data-slot="command-item"
        data-value="copy"
        data-keywords="duplicate"
      >
        Copy
        <span data-slot="command-shortcut">⌘C</span>
      </div>
      <div
        data-slot="command-item"
        data-value="paste"
        data-keywords="insert"
      >
        Paste
        <span data-slot="command-shortcut">⌘V</span>
      </div>
    </div>

    <div data-slot="command-separator"></div>

    <div data-slot="command-group">
      <div data-slot="command-group-heading">Navigation</div>
      <div data-slot="command-item" data-value="home">
        Home
        <span data-slot="command-shortcut">⌘H</span>
      </div>
      <div data-slot="command-item" data-value="settings">
        Settings
        <span data-slot="command-shortcut">⌘,</span>
      </div>
    </div>

    <div data-slot="command-item" data-disabled>
      Disabled item
    </div>
  </div>
</div>

Data Slots

SlotRequiredDescription
commandYesRoot container.
command-input-wrapperNoWrapper around the search input.
command-inputYesSearch input that drives filtering and ranking.
command-listYesScrollable container for items, groups, separators, and the empty slot.
command-itemYesSelectable result item. Value is inferred from text content or set via data-value.
command-groupNoWraps a set of related items with a heading.
command-group-headingNoNon-interactive heading label for a group.
command-emptyNoMessage shown when no items match the search.
command-separatorNoVisual divider between groups or items.
command-shortcutNoDisplays a keyboard shortcut hint; excluded from search text.

Item Data Attributes

AttributeDescription
data-valueExplicit value used for selection and events. Falls back to inferred text content.
data-keywordsComma or newline separated extra terms that boost the item's fuzzy-search rank without being visible in the label.
data-disabledMarks the item as non-selectable.
data-force-mountKeeps the item in the DOM even when filtered out (hidden via hidden).
data-always-renderKeeps the item in the DOM and visible even when filtered out.

Generated Attributes

AttributeElement(s)Description
data-selectedcommand-itemPresent on the currently active (highlighted) item.
hiddencommand-item, command-groupToggled based on whether the item or group matches the current search.
rolecommand-listSet to "listbox".
rolecommand-itemSet to "option".
rolecommand-groupSet to "group".
aria-selectedcommand-item"true" when selected, "false" otherwise.
aria-labelledbycommand-groupPoints to the group heading id.
aria-disabledcommand-item"true" when the item is disabled.

Events

Outbound

EventDetailDescription
command:select{ value: string }Fired on the root when an item is selected via click or Enter.
command:change{ value: string }Fired on the root when the active (highlighted) item changes.

Inbound

EventDetailDescription
command:set{ value: string }Dispatch on the root to programmatically set the active item.

API Reference

Options

OptionTypeDefaultDescription
defaultValuestringInitial active item value.
labelstringAccessible label announced to assistive technology. Defaults to the data-label attribute on the root.
shouldFilterbooleantrueDisable built-in filtering and sorting when set to false.
loopbooleanfalseWrap arrow-key navigation from last to first item and vice versa.
disablePointerSelectionbooleanfalseIgnore pointer-move events for selection, keeping interaction keyboard-only.
vimBindingsbooleantrueEnable Ctrl+J, Ctrl+K, Ctrl+N, and Ctrl+P shortcuts for vertical navigation.
filter(value: string, search: string, keywords?: string[]) => numberCustom ranking function. Return 0 to hide the item. Higher scores rank higher.
onValueChange(value: string | null) => voidCalled when the active item value changes.
onSearchChange(search: string) => voidCalled when the search query changes.
onSelect(value: string) => voidCalled when an item is selected via click or Enter.

Controller

MemberTypeDescription
valuestring | null (getter)Current active item value.
searchstring (getter)Current search query.
select(value)(value: string | null) => voidSet the active item value programmatically. Passing null clears the selection.
setSearch(value)(search: string) => voidSet the search query programmatically.
destroy()() => voidRemove all listeners, mutation observers, and clean up.

Controller

The controller is returned by Command.createCommand() and provides imperative control over the command menu's state.

const controller = Command.createCommand(root, {
  label: "Command Menu",
});

// Set search query programmatically
controller.setSearch("copy");

// Select an item programmatically
controller.select("paste");

// Clear selection
controller.select(null);

// Read current state
console.log(controller.value); // "paste" | null
console.log(controller.search); // "copy"

// Clean up
controller.destroy();