Command Palette
A Cmd+K command palette for actions, links, and typed input commands.
You declare commands once and render the returned island. Open it with a trigger in this guide. Selecting an input command opens a generated Areia form. The same command can register as a WebMCP tool so an agent runs the validated operation in this page and session.
Installation
npm install @areia/cmdpnpm add @areia/cmdyarn add @areia/cmdbun add @areia/cmdPeer dependencies: areia, ilha, @areia/slots, and @areia/form. Install @standard-schema/spec when you declare input commands; your schema library implements it.
For a single package with no Ilha, Areia, or Tailwind install, import @areia/cmd/standalone and @areia/cmd/standalone.css. That bundle includes Ilha (html, ilha, mount) so you do not add those packages. Do not mix it with @areia/cmd on the same page.
Import
import {
CommandPalette,
CommandPaletteBase,
type InputCommand,
} from "@areia/cmd";
Usage
Call CommandPalette with your commands and render the island. The factory keeps commands and callbacks in its closure, so they never serialize through data-ilha-props. Filtering, ranking, keyboard navigation, and ARIA state come from the @areia/slots command primitive. Dialog behavior comes from the @areia/slots dialog primitive.
import { html, ilha } from "ilha";
import { CommandPalette } from "@areia/cmd";
const AppCommands = CommandPalette(
[
{
id: "refresh_dashboard",
label: "Refresh dashboard",
group: "Actions",
run: ({ signal }) => refreshDashboard({ signal }),
},
{
id: "open_billing",
label: "Open billing settings",
group: "Navigation",
href: "/settings/billing",
},
],
{
hotkey: false,
trigger: html`<button type="button">Open commands</button>`,
},
);
export default ilha(() => <AppCommands />);Try Search settings in the palette to open the generated form.
Actions and links
An action command has run. A link command has href. Give each command a unique id — it is the selection value and the WebMCP tool name.
const commands = [
{
id: "sign_out",
label: "Sign out",
run: () => signOut(),
},
{
id: "open_billing",
label: "Open billing settings",
href: "/settings/billing",
},
];
Selection closes the palette before run executes. You own progress and errors — surface them through your toast or state layer. Link commands navigate with window.location.assign. Set external: true to open a new tab with rel="noopener noreferrer". External links render a ↗ after the label.
const commands = [
{
id: "open_docs",
label: "Open documentation",
href: "https://ilha.build",
external: true,
},
];
Groups, keywords, icons, and shortcuts
Commands with the same group share a heading. Groups appear in the order of their first command; commands keep authored order. Commands without group have no heading. keywords extend the filter, icon renders before the label, and shortcut is display text only.
const commands = [
{
id: "refresh_dashboard",
label: "Refresh dashboard",
description: "Reload the latest dashboard data.",
group: "Actions",
keywords: ["reload", "sync"],
shortcut: "⌘R",
icon: <RefreshIcon />,
run: ({ signal }) => refreshDashboard({ signal }),
},
];
Open the palette
The palette toggles on Cmd+K or Ctrl+K by default. Pass hotkey: false when you open it yourself. Prefer trigger for a button — the dialog controller wires click, ARIA, and focus return.
import { html } from "ilha";
const AppCommands = CommandPalette(commands, {
hotkey: false,
trigger: html`<button type="button">Open commands</button>`,
});
Dispatch events on that palette’s dialog root when another controller owns the trigger. Events stay scoped to that palette.
const palette = document.querySelector<HTMLElement>(
'[data-slot="dialog"]',
);
palette?.dispatchEvent(new CustomEvent("command-palette:open"));
palette?.dispatchEvent(
new CustomEvent("command-palette:toggle"),
);
palette?.dispatchEvent(
new CustomEvent("command-palette:close"),
);
Collect command input
Add an InputCommand when the operation needs structured input. Selecting it opens a second step generated by @areia/form. Agents get the same command as a WebMCP tool.
The schema must implement Standard Schema for validation and Standard JSON Schema for discovery. Zod implements both.
import { CommandPalette, type InputCommand } from "@areia/cmd";
import { z } from "zod";
const searchInput = z.object({
query: z
.string()
.min(1)
.describe("Settings text to search for"),
limit: z.number().int().min(1).max(20).default(5),
});
const searchSettings = {
id: "search_settings",
label: "Search settings",
description: "Find a setting by name.",
input: searchInput,
defaultValues: { query: "", limit: 5 },
submitLabel: "Search",
webmcp: {
description:
"Search the signed-in user's application settings.",
annotations: { readOnlyHint: true },
},
run: async ({ query, limit }, { signal }) =>
searchApplicationSettings({ query, limit, signal }),
} satisfies InputCommand<typeof searchInput>;
const AppCommands = CommandPalette([
...commands,
searchSettings,
]);
A valid submission closes the palette. Escape or the back arrow returns you to the command list without closing the palette. Configure the form with defaultValues, uiOverrides, and submitLabel. WebMCP calls convert the schema to JSON Schema and validate before run. Both paths receive the schema’s output. Keep authorization inside run.
Expose commands to agents with WebMCP
Without webmcp, a command stays private to the palette. Add a webmcp object with an agent-facing description to register it through document.modelContext. That includes input commands — omit webmcp if the form is only for people.
const commands = [
{
id: "refresh_dashboard",
label: "Refresh dashboard",
run: ({ signal }) => refreshDashboard({ signal }),
webmcp: {
description:
"Refresh the signed-in user's dashboard data.",
annotations: {
readOnlyHint: true,
untrustedContentHint: true,
},
},
},
];
WebMCP is progressive enhancement. When document.modelContext is missing, the palette still works — nothing warns, throws, or polyfills. Disabled commands never register. Return enough from run for an agent to verify the outcome.
Handle execution context
run receives the invocation source and an abort signal. Pass signal to cancellable work. Use source only when presentation differs; keep authorization identical.
const commands = [
{
id: "refresh_dashboard",
label: "Refresh dashboard",
run: async ({ source, signal }) => {
await refreshDashboard({ signal });
if (source === "palette")
showToast("Dashboard refreshed");
return { refreshed: true };
},
},
];
Security
- Treat
webmcpas an exposure boundary, not presentation metadata. - Keep authentication, authorization, validation, and audit inside
run. - State consequential side effects in the agent-facing
description. readOnlyHintanduntrustedContentHintare hints, not controls.
Headless flavor
CommandPaletteBase uses the same commands, options, controllers, hotkey, trigger, form step, and WebMCP wiring with unstyled slot markup.
import { CommandPaletteBase } from "@areia/cmd";
const BareCommands = CommandPaletteBase(commands);
[data-slot="command-item"][data-selected="true"] {
background: var(--areia-primary-soft);
color: var(--areia-primary-soft-foreground);
}
Command properties
| Prop | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Unique selection value and WebMCP tool name. |
label |
string |
Yes | Visible command label. |
run |
(context) => unknown | Promise<…> |
Action | Executes an action command. |
href |
string |
Link | Navigates when the command is selected. |
external |
boolean |
No | Opens a link in a new tab. |
description |
string |
No | Supporting text shown below the label. |
group |
string |
No | Groups commands under a shared heading. |
keywords |
readonly string[] |
No | Additional filtering terms. |
shortcut |
string |
No | Display-only shortcut text. |
icon |
RawHtml | string |
No | Content rendered before the label. |
disabled |
boolean |
No | Prevents palette execution and WebMCP registration. |
webmcp |
WebMCPExposure |
No | Exposes a command through WebMCP. |
input |
CommandInputSchema |
Input | Validates form input and provides agent JSON Schema. |
defaultValues |
InferOutput<Schema> |
No | Initial values for the generated form. |
uiOverrides |
UIOverrides |
No | Overrides inferred @areia/form fields. |
submitLabel |
string |
No | Generated form submit button text. |
A basic command defines exactly one of run or href. An InputCommand defines input and run. Add webmcp when an agent should call it too.
Options
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string |
"Command palette" |
Accessible name for the dialog and command input. |
placeholder |
string |
"Type a command or search…" |
Input placeholder text. |
empty |
string |
"No results found." |
Text shown when the filter matches nothing. |
trigger |
RawHtml |
— | Click-to-open trigger content, rendered as [data-slot="dialog-trigger"]. |
hotkey |
"mod+k" | false |
"mod+k" |
Global Cmd+K / Ctrl+K toggle. false disables it. |
loop |
boolean |
false |
Wrap arrow-key selection from the last item to first. |
Cleanup
Unmount destroys both controllers, removes the hotkey listener, and aborts WebMCP registrations. Mount one palette per command set so tool names stay unique.