implement
Command

Command

A searchable command palette, filtered and ranked as you type.

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}`)),
		),
	);
}

Installation

npx jsrepo add @implementjs/ui/command

It installs @implementjs/lucide at the same time.

Usage

Command is a scored list over a search box: every item is matched against the query, the losers are hidden, and the survivors are re-ordered. The styled layer supplies the popover surface, the search row (icon, border, and a default placeholder), and a list capped at 20rem with its own scrolling.

import {
	Command,
	CommandEmpty,
	CommandGroup,
	CommandGroupHeading,
	CommandGroupItems,
	CommandInput,
	CommandItem,
	CommandList,
} from "@/lib/components/ui/command";

Command(
	{ label: "Command palette" },
	CommandInput({ placeholder: "Type a command..." }),
	CommandList(
		CommandEmpty("No results found."),
		CommandGroup(
			CommandGroupHeading("Suggestions"),
			CommandGroupItems(
				CommandItem({ value: "calendar", onSelect: open }, "Calendar"),
				CommandItem({ value: "search", onSelect: open }, "Search"),
			),
		),
	),
);

In a dialog

A palette is usually a modal. Put the command inside a dialog and drop the dialog's own padding, so the search row sits flush against the top edge:

Dialog(
	{ open },
	DialogContent(
		{ class: "p-0", showCloseButton: false },
		Command({ label: "Command palette" }, CommandInput(), CommandList(/* ... */)),
	),
);

Grid mode

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}`)),
		),
	);
}

columns lays the items out in a grid and turns the arrow keys two-dimensional โ€” what an emoji or icon picker wants. CommandGroupItems is where the grid classes go, since it owns the item row.

API Reference

Every prop the styling does not consume is forwarded to the Command primitive, so the tables below are the whole surface โ€” the behavior props and the styling ones together.

Command

The root. Owns the search and the highlighted value, scores every item against the search, and handles the keyboard. Styled as a rounded popover surface that clips its list. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
labelstringโ€”An accessible label for the menu. Not visible; read to screen readers.
valueSignal<string> | stringโ€”The value of the highlighted item. Pass a signal to control or observe it from outside.
searchSignal<string> | stringโ€”The 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. The styled input arrives wrapped in a bordered row with a search icon, and defaults its placeholder to "Type to 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. Styled with a 20rem cap and its own vertical scrolling. 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
valuestringโ€”A 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
valuestringโ€”A 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() => voidโ€”Runs 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
valuestringโ€”A 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() => voidโ€”Runs 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