implement
Command

Command

A filterable command menu that scores, sorts, and navigates items from a search input.

Command menu

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)),
			),
		),
	),
);
Emoji picker

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.

PropTypeDefaultDescription
labelstringAn accessible label for the menu. Not visible; read to screen readers.
valueSignal<string> | stringThe value of the highlighted item. Pass a signal to control or observe it from outside.
searchSignal<string> | stringThe search query. Pass a signal to control or observe it from outside; CommandInput binds to it.
shouldFilterbooleantrueSet to false to turn off the automatic filtering and sorting, and conditionally render valid items yourself.
filter(value: string, search: string, keywords?: string[]) => numbercomputeCommandScoreCustom scoring. Return a number between 0 and 1; 0 hides the item entirely.
loopbooleanfalseWhether keyboard navigation wraps around at both ends.
disablePointerSelectionbooleanfalseWhen true, moving the pointer over an item does not highlight it.
vimBindingsbooleantrueCtrl+n/j/p/k (and ctrl+h/l in a grid) move the highlight.
columnsnumber | null | Readable<number | null>nullThe number of columns the items are laid out in. Turns on grid navigation; match it to your CSS layout.
disableInitialScrollbooleanfalseWhen true, the initial highlight is not scrolled into view.
Data attributeValue
[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 attributeValue
[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.

PropTypeDefaultDescription
labelstring"Suggestions"Accessible name for the listbox.
Data attributeValue
[data-command-list]Present
CSS variableDescription
--ip-command-list-heightThe 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 attributeValue
[data-command-viewport]Present

CommandEmpty

Shown only when the search leaves no items visible. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
forceMountbooleanfalseRender even while items match.
Data attributeValue
[data-command-empty]Present

CommandLoading

A progress region for async items. Sets role="progressbar". Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
progressnumber | Readable<number>0Progress between 0 and 100.
Data attributeValue
[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.

PropTypeDefaultDescription
valuestringA unique value naming the group, used to sort groups by their best match. Defaults to the group's id.
forceMountbooleanfalseKeep the group while every item in it is filtered out.
Data attributeValue
[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 attributeValue
[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 attributeValue
[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.

PropTypeDefaultDescription
valuestringA unique value used to filter and rank the item. Defaults to the item's text content; dynamic children need an explicit stable value.
keywordsstring[]Extra terms the filter also scores.
disabledSignal<boolean> | booleanfalsePrevents choosing the item; the keyboard skips it.
onSelect() => voidRuns when the item is chosen, by click or by Enter.
forceMountbooleanfalseKeep the item visible regardless of the search.
Data attributeValue
[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.

PropTypeDefaultDescription
valuestringA unique value used to filter and rank the item. Defaults to the item's text content.
keywordsstring[]Extra terms the filter also scores.
disabledSignal<boolean> | booleanfalsePrevents choosing the item; the keyboard skips it.
onSelect() => voidRuns when the item is chosen, by click or by Enter.
Data attributeValue
[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.

PropTypeDefaultDescription
forceMountbooleanfalseKeep the separator while searching.
Data attributeValue
[data-command-separator]Present