Command
Fast, composable command menu with ranked fuzzy search, keyboard-first navigation, and smart result sorting.
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
| Slot | Required | Description |
|---|---|---|
command |
Yes | Root container. |
command-input-wrapper |
No | Wrapper around the search input. |
command-input |
Yes | Search input that drives filtering and ranking. |
command-list |
Yes | Scrollable container for items, groups, separators, and the empty slot. |
command-item |
Yes | Selectable result item. Value is inferred from text content or set via data-value. |
command-group |
No | Wraps a set of related items with a heading. |
command-group-heading |
No | Non-interactive heading label for a group. |
command-empty |
No | Message shown when no items match the search. |
command-separator |
No | Visual divider between groups or items. |
command-shortcut |
No | Displays a keyboard shortcut hint; excluded from search text. |
Item Data Attributes
| Attribute | Description |
|---|---|
data-value |
Explicit value used for selection and events. Falls back to inferred text content. |
data-keywords |
Comma or newline separated extra terms that boost the item’s fuzzy-search rank without being visible in the label. |
data-disabled |
Marks the item as non-selectable. |
data-force-mount |
Keeps the item in the DOM even when filtered out (hidden via hidden). |
data-always-render |
Keeps the item in the DOM and visible even when filtered out. |
Generated Attributes
| Attribute | Element(s) | Description |
|---|---|---|
data-selected |
command-item |
Present on the currently active (highlighted) item. |
hidden |
command-item, command-group |
Toggled based on whether the item or group matches the current search. |
role |
command-list |
Set to "listbox". |
role |
command-item |
Set to "option". |
role |
command-group |
Set to "group". |
aria-selected |
command-item |
"true" when selected, "false" otherwise. |
aria-labelledby |
command-group |
Points to the group heading id. |
aria-disabled |
command-item |
"true" when the item is disabled. |
Events
Outbound
| Event | Detail | Description |
|---|---|---|
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
| Event | Detail | Description |
|---|---|---|
command:set |
{ value: string } |
Dispatch on the root to programmatically set the active item. |
API Reference
Options
| Option | Type | Default | Description |
|---|---|---|---|
defaultValue |
string |
— | Initial active item value. |
label |
string |
— | Accessible label announced to assistive technology. Defaults to the data-label attribute on the root. |
shouldFilter |
boolean |
true |
Disable built-in filtering and sorting when set to false. |
loop |
boolean |
false |
Wrap arrow-key navigation from last to first item and vice versa. |
disablePointerSelection |
boolean |
false |
Ignore pointer-move events for selection, keeping interaction keyboard-only. |
vimBindings |
boolean |
true |
Enable Ctrl+J, Ctrl+K, Ctrl+N, and Ctrl+P shortcuts for vertical navigation. |
filter |
(value: string, search: string, keywords?: string[]) => number |
— | Custom ranking function. Return 0 to hide the item. Higher scores rank higher. |
onValueChange |
(value: string | null) => void |
— | Called when the active item value changes. |
onSearchChange |
(search: string) => void |
— | Called when the search query changes. |
onSelect |
(value: string) => void |
— | Called when an item is selected via click or Enter. |
Controller
| Member | Type | Description |
|---|---|---|
value |
string | null (getter) |
Current active item value. |
search |
string (getter) |
Current search query. |
select(value) |
(value: string | null) => void |
Set the active item value programmatically. Passing null clears the selection. |
setSearch(value) |
(search: string) => void |
Set the search query programmatically. |
destroy() |
() => void |
Remove 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();