Command
A filterable command menu that scores, sorts, and navigates items from a search input.
Nothing selected yet
import { Div, P, signal } from "@implementjs/core";
import {
CalculatorIcon,
CalendarIcon,
CreditCardIcon,
SettingsIcon,
SmileIcon,
UserIcon,
type IconComponent,
} from "@implementjs/lucide";
import {
Command,
CommandEmpty,
CommandGroup,
CommandGroupHeading,
CommandGroupItems,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandViewport,
} from "@/lib/components/ui/command";
export default function CommandDemo() {
const lastSelected = signal<string | null>(null);
const item = (value: string, icon: IconComponent) =>
CommandItem(
{ value, onSelect: () => lastSelected.set(value) },
icon({ class: "text-muted-foreground", "aria-hidden": true }),
value,
);
return Div(
{ class: "flex w-full max-w-md flex-col items-center gap-3" },
Command(
{ label: "Command menu", class: "rounded-lg border shadow-md" },
CommandInput({ placeholder: "Type a command or search..." }),
CommandList(
CommandViewport(
CommandEmpty("No results found."),
CommandGroup(
{ value: "suggestions" },
CommandGroupHeading("Suggestions"),
CommandGroupItems(
item("Calendar", CalendarIcon),
item("Search Emoji", SmileIcon),
item("Calculator", CalculatorIcon),
),
),
CommandSeparator(),
CommandGroup(
{ value: "settings" },
CommandGroupHeading("Settings"),
CommandGroupItems(
item("Profile", UserIcon),
item("Billing", CreditCardIcon),
item("Settings", SettingsIcon),
),
),
),
),
),
P(
{ class: "text-sm text-muted-foreground" },
lastSelected.bind((value) => (value == null ? "Nothing selected yet" : `Selected: ${value}`)),
),
);
}A command menu is a search input over a list of actions. Command is the root, CommandInput is the search box, CommandList is the results region, CommandViewport wraps everything inside the list, and CommandItem is one choice. Typing scores every item against the search, hides the misses, and sorts the best matches to the top. A highlight follows the keyboard — the arrow keys move it, Enter chooses it — while focus stays in the input.
import {
Command,
CommandEmpty,
CommandInput,
CommandItem,
CommandList,
CommandViewport,
} from "@implementjs/primitives";
Command(
{ label: "Command menu" },
CommandInput({ placeholder: "Type a command..." }),
CommandList(
CommandViewport(
CommandEmpty("No results found."),
CommandItem({ value: "calendar", onSelect: () => openCalendar() }, "Calendar"),
CommandItem({ value: "settings", onSelect: () => openSettings() }, "Settings"),
),
),
);
Each part accepts optional props and children — pass a props object when you need attributes, or pass children directly. See createComponent. Extra props are forwarded onto the underlying Div, Input, or A.
Unlike the popup primitives, Command renders in place — there is no trigger and no open state. Put it inside a Dialog to build a ⌘K palette.
Search and value
The root owns two strings: search (what is typed) and value (the highlighted item). Pass signals to control or observe either from outside:
const search = signal("");
const value = signal("");
Command(
{ search, value },
CommandInput(),
CommandList(CommandViewport(CommandItem({ value: "calendar" }, "Calendar"))),
);
search.set("cal"); // filters the list
value.onChange((current) => console.log("highlighted", current));
Every item needs a stable, unique value; it is what the search is scored against (along with keywords) and what the root tracks as the highlight. When omitted, the item's text content is used.
Filtering
Scoring uses computeCommandScore, which favors continuous matches and word starts — typing "cal" ranks "Calendar" above "Local time". An item whose score is 0 gets the hidden attribute; the rest are re-ordered in the DOM by score, best first. Clearing the search restores the original order.
Pass keywords for aliases the search should also match, and filter to replace the scoring entirely:
CommandItem({ value: "trash", keywords: ["delete", "remove"] }, "Trash");
Command({
filter: (value, search) => (value.startsWith(search) ? 1 : 0),
});
shouldFilter: false turns filtering and sorting off — useful when a server does the searching and you render only matching items yourself.
CommandEmpty renders only when the search leaves nothing visible. CommandLoading is a role="progressbar" region for async items. CommandSeparator hides itself while a search is active.
Groups
CommandGroup wraps a CommandGroupHeading and a CommandGroupItems. The heading names the group (aria-labelledby points at it), and the whole group hides once every item inside it is filtered out. Groups need a unique value of their own when the search should sort them (best group first):
CommandGroup(
{ value: "settings" },
CommandGroupHeading("Settings"),
CommandGroupItems(
CommandItem({ value: "profile" }, "Profile"),
CommandItem({ value: "billing" }, "Billing"),
),
);
Keyboard
Focus stays in the input; keys bubble to the root. ArrowDown/ArrowUp move the highlight, Home/End jump to the first and last item, and Enter chooses the highlighted item (it runs that item's onSelect). Alt+arrow jumps by group, Meta+arrow to the ends. Ctrl+n/j and Ctrl+p/k mirror the arrows; pass vimBindings: false to turn them off. loop: true wraps at both ends. Disabled items are skipped.
Grid mode
Pass columns to navigate the items as a grid: ArrowLeft/ArrowRight move within a row, ArrowUp/ArrowDown move between rows keeping the column, and each group starts a new row. The primitive tracks the grid logically — lay the items out with CSS to match, e.g. grid-cols-5 when columns is 5:
Command(
{ columns: 5 },
CommandInput(),
CommandList(
CommandViewport(
CommandGroupItems(
{ class: "grid grid-cols-5" },
...emojis.map((emoji) => CommandItem({ value: emoji.name }, emoji.char)),
),
),
),
);
Pick an emoji
import { Div, P, signal, Span } from "@implementjs/core";
import {
Command,
CommandEmpty,
CommandGroup,
CommandGroupHeading,
CommandGroupItems,
CommandInput,
CommandItem,
CommandList,
CommandViewport,
} from "@/lib/components/ui/command";
const COLUMNS = 5;
const sections: { name: string; emojis: [name: string, emoji: string][] }[] = [
{
name: "Smileys",
emojis: [
["grinning face", "😀"],
["face with tears of joy", "😂"],
["smiling face with hearts", "🥰"],
["thinking face", "🤔"],
["sleeping face", "😴"],
["face with sunglasses", "😎"],
["party face", "🥳"],
],
},
{
name: "Animals",
emojis: [
["dog", "🐶"],
["cat", "🐱"],
["fox", "🦊"],
["panda", "🐼"],
["penguin", "🐧"],
["octopus", "🐙"],
],
},
{
name: "Food",
emojis: [
["pizza", "🍕"],
["taco", "🌮"],
["sushi", "🍣"],
["doughnut", "🍩"],
["avocado", "🥑"],
],
},
];
export default function CommandGridDemo() {
const picked = signal<string | null>(null);
return Div(
{ class: "flex w-full max-w-md flex-col items-center gap-3" },
Command(
{ label: "Emoji picker", columns: COLUMNS, class: "rounded-lg border shadow-md" },
CommandInput({ placeholder: "Search emoji..." }),
CommandList(
CommandViewport(
CommandEmpty("No emoji found."),
...sections.map((section) =>
CommandGroup(
{ value: section.name },
CommandGroupHeading(section.name),
CommandGroupItems(
// keep the CSS columns in step with the `columns` prop on the root
{ class: "grid grid-cols-5" },
...section.emojis.map(([name, emoji]) =>
CommandItem(
{
value: name,
onSelect: () => picked.set(`${emoji} ${name}`),
// square cells, so the highlight reads as a grid rather than rows
class: "aspect-square justify-center text-xl",
},
Span({ "aria-hidden": true }, emoji),
Span({ class: "sr-only" }, name),
),
),
),
),
),
),
),
),
P(
{ class: "text-sm text-muted-foreground" },
picked.bind((value) => (value == null ? "Pick an emoji" : `Picked: ${value}`)),
),
);
}Items that navigate
CommandLinkItem renders an anchor instead of a Div, so choosing it navigates. Enter clicks the highlighted element, which follows the link:
CommandLinkItem({ value: "docs", href: "/docs" }, "Documentation");
Styling
The highlighted item sets data-selected; disabled items set data-disabled and aria-disabled. Filtered-out parts (items, groups, the empty state, separators) get the native hidden attribute — keep it winning over any display utility classes (Tailwind's preflight already does). A CommandViewport directly inside the list reports its height as --ip-command-list-height on the list, for animating the list as results come and go.
CommandItem(
{
value: "calendar",
class:
"rounded-sm px-2 py-1.5 text-sm data-selected:bg-accent data-disabled:pointer-events-none data-disabled:opacity-50",
},
"Calendar",
);
API Reference
Command
The root. Owns the search and the highlighted value, scores every item against the search, and handles the keyboard. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | An accessible label for the menu. Not visible; read to screen readers. |
value | Signal<string> | string | — | The value of the highlighted item. Pass a signal to control or observe it from outside. |
search | Signal<string> | string | — | The search query. Pass a signal to control or observe it from outside; CommandInput binds to it. |
shouldFilter | boolean | true | Set to false to turn off the automatic filtering and sorting, and conditionally render valid items yourself. |
filter | (value: string, search: string, keywords?: string[]) => number | computeCommandScore | Custom scoring. Return a number between 0 and 1; 0 hides the item entirely. |
loop | boolean | false | Whether keyboard navigation wraps around at both ends. |
disablePointerSelection | boolean | false | When true, moving the pointer over an item does not highlight it. |
vimBindings | boolean | true | Ctrl+n/j/p/k (and ctrl+h/l in a grid) move the highlight. |
columns | number | null | Readable<number | null> | null | The number of columns the items are laid out in. Turns on grid navigation; match it to your CSS layout. |
disableInitialScroll | boolean | false | When true, the initial highlight is not scrolled into view. |
| Data attribute | Value |
|---|---|
[data-command-root] | Present |
CommandInput
The search box. Sets role="combobox" with aria-activedescendant on the highlighted item; two-way binds the root's search. Renders a Input; extra props are forwarded onto it.
| Data attribute | Value |
|---|---|
[data-command-input] | Present |
CommandList
The scrollable results region. Sets role="listbox". Give it a max height and overflow to make it scroll. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "Suggestions" | Accessible name for the listbox. |
| Data attribute | Value |
|---|---|
[data-command-list] | Present |
| CSS variable | Description |
|---|---|
--ip-command-list-height | The measured height of the viewport, written on the list. Animate the list's height with it. |
CommandViewport
The list's sole child, wrapping all groups and items. Its measured height feeds --ip-command-list-height. Renders a Div; extra props are forwarded onto it.
| Data attribute | Value |
|---|---|
[data-command-viewport] | Present |
CommandEmpty
Shown only when the search leaves no items visible. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
forceMount | boolean | false | Render even while items match. |
| Data attribute | Value |
|---|---|
[data-command-empty] | Present |
CommandLoading
A progress region for async items. Sets role="progressbar". Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
progress | number | Readable<number> | 0 | Progress between 0 and 100. |
| Data attribute | Value |
|---|---|
[data-command-loading] | Present |
CommandGroup
Wraps a heading and its items. Hidden once the search filters out every item inside it. In a grid, each group starts a new row. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | A unique value naming the group, used to sort groups by their best match. Defaults to the group's id. |
forceMount | boolean | false | Keep the group while every item in it is filtered out. |
| Data attribute | Value |
|---|---|
[data-command-group] | Present |
CommandGroupHeading
The group's visible name; the group's items point aria-labelledby at it. Renders a Div; extra props are forwarded onto it.
| Data attribute | Value |
|---|---|
[data-command-group-heading] | Present |
CommandGroupItems
The container for a group's items. Sets role="group". Renders a Div; extra props are forwarded onto it.
| Data attribute | Value |
|---|---|
[data-command-group-items] | Present |
CommandItem
One choice. Sets role="option". Filtered and ranked against its value (or text content) plus keywords; hidden when its score is 0. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | A unique value used to filter and rank the item. Defaults to the item's text content; dynamic children need an explicit stable value. |
keywords | string[] | — | Extra terms the filter also scores. |
disabled | Signal<boolean> | boolean | false | Prevents choosing the item; the keyboard skips it. |
onSelect | () => void | — | Runs when the item is chosen, by click or by Enter. |
forceMount | boolean | false | Keep the item visible regardless of the search. |
| Data attribute | Value |
|---|---|
[data-command-item] | Present |
[data-selected] | Present on the highlighted item |
[data-disabled] | Present when disabled |
[data-value] | The item's value |
[data-group] | The value of the group the item belongs to |
CommandLinkItem
A CommandItem that renders an anchor, for items that navigate. Enter clicks it, which follows the link. Renders a A; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | A unique value used to filter and rank the item. Defaults to the item's text content. |
keywords | string[] | — | Extra terms the filter also scores. |
disabled | Signal<boolean> | boolean | false | Prevents choosing the item; the keyboard skips it. |
onSelect | () => void | — | Runs when the item is chosen, by click or by Enter. |
| Data attribute | Value |
|---|---|
[data-command-item] | Present |
[data-selected] | Present on the highlighted item |
[data-disabled] | Present when disabled |
CommandSeparator
A divider between groups. Hidden while a search is active. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
forceMount | boolean | false | Keep the separator while searching. |
| Data attribute | Value |
|---|---|
[data-command-separator] | Present |