Command
A searchable command palette, filtered and ranked as you type.
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.
Copy the file below to src/lib/components/ui/command.ts. It imports cn, so copy utils.ts to src/lib/utils.ts too. Then, on top of @implementjs/core and @implementjs/primitives:
npm install @implementjs/lucide
import { Div, type Child, type ComponentProps } from "@implementjs/core";
import { SearchIcon } from "@implementjs/lucide";
import {
Command as CommandPrimitive,
CommandEmpty as CommandEmptyPrimitive,
CommandGroup as CommandGroupPrimitive,
CommandGroupHeading as CommandGroupHeadingPrimitive,
CommandGroupItems as CommandGroupItemsPrimitive,
CommandInput as CommandInputPrimitive,
CommandItem as CommandItemPrimitive,
CommandLinkItem as CommandLinkItemPrimitive,
CommandList as CommandListPrimitive,
CommandLoading as CommandLoadingPrimitive,
CommandSeparator as CommandSeparatorPrimitive,
CommandViewport as CommandViewportPrimitive,
} from "@implementjs/primitives";
import { createComponent } from "@implementjs/primitives";
import { cn } from "@/lib/utils";
export type CommandProps = ComponentProps<typeof CommandPrimitive>;
export type CommandInputProps = ComponentProps<typeof CommandInputPrimitive>;
export type CommandListProps = ComponentProps<typeof CommandListPrimitive>;
export type CommandViewportProps = ComponentProps<typeof CommandViewportPrimitive>;
export type CommandEmptyProps = ComponentProps<typeof CommandEmptyPrimitive>;
export type CommandLoadingProps = ComponentProps<typeof CommandLoadingPrimitive>;
export type CommandGroupProps = ComponentProps<typeof CommandGroupPrimitive>;
export type CommandGroupHeadingProps = ComponentProps<typeof CommandGroupHeadingPrimitive>;
export type CommandGroupItemsProps = ComponentProps<typeof CommandGroupItemsPrimitive>;
export type CommandItemProps = ComponentProps<typeof CommandItemPrimitive>;
export type CommandLinkItemProps = ComponentProps<typeof CommandLinkItemPrimitive>;
export type CommandSeparatorProps = ComponentProps<typeof CommandSeparatorPrimitive>;
export const Command = createComponent(function Command(
{ class: className, ...props }: CommandProps,
...children: Child[]
) {
return CommandPrimitive(
{
...props,
"data-slot": "command",
class: cn(
"flex size-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
),
},
...children,
);
});
export const CommandInput = createComponent(function CommandInput({
class: className,
...props
}: CommandInputProps) {
return Div(
{ "data-slot": "command-input-wrapper", class: "flex h-11 items-center gap-2 border-b px-3" },
SearchIcon({ class: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }),
CommandInputPrimitive({
placeholder: "Type to search...",
...props,
"data-slot": "command-input",
class: cn(
"flex h-11 w-full bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
),
}),
);
});
export const CommandList = createComponent(function CommandList(
{ class: className, ...props }: CommandListProps,
...children: Child[]
) {
return CommandListPrimitive(
{
...props,
"data-slot": "command-list",
class: cn("max-h-80 scroll-py-1 overflow-x-hidden overflow-y-auto", className),
},
...children,
);
});
export const CommandViewport = createComponent(function CommandViewport(
{ class: className, ...props }: CommandViewportProps,
...children: Child[]
) {
return CommandViewportPrimitive(
{ ...props, "data-slot": "command-viewport", class: cn(className) },
...children,
);
});
export const CommandEmpty = createComponent(function CommandEmpty(
{ class: className, ...props }: CommandEmptyProps,
...children: Child[]
) {
return CommandEmptyPrimitive(
{
...props,
"data-slot": "command-empty",
class: cn("py-6 text-center text-sm text-muted-foreground", className),
},
...children,
);
});
export const CommandLoading = createComponent(function CommandLoading(
{ class: className, ...props }: CommandLoadingProps,
...children: Child[]
) {
return CommandLoadingPrimitive(
{
...props,
"data-slot": "command-loading",
class: cn("py-6 text-center text-sm text-muted-foreground", className),
},
...children,
);
});
export const CommandGroup = createComponent(function CommandGroup(
{ class: className, ...props }: CommandGroupProps,
...children: Child[]
) {
return CommandGroupPrimitive(
{ ...props, "data-slot": "command-group", class: cn("overflow-hidden", className) },
...children,
);
});
export const CommandGroupHeading = createComponent(function CommandGroupHeading(
{ class: className, ...props }: CommandGroupHeadingProps,
...children: Child[]
) {
return CommandGroupHeadingPrimitive(
{
...props,
"data-slot": "command-group-heading",
class: cn("px-3 pt-3 pb-1 text-xs font-medium text-muted-foreground uppercase", className),
},
...children,
);
});
export const CommandGroupItems = createComponent(function CommandGroupItems(
{ class: className, ...props }: CommandGroupItemsProps,
...children: Child[]
) {
return CommandGroupItemsPrimitive(
{ ...props, "data-slot": "command-group-items", class: cn("p-1", className) },
...children,
);
});
const itemClass =
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none data-selected:bg-accent data-selected:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
export const CommandItem = createComponent(function CommandItem(
{ class: className, ...props }: CommandItemProps,
...children: Child[]
) {
return CommandItemPrimitive(
{ ...props, "data-slot": "command-item", class: cn(itemClass, className) },
...children,
);
});
export const CommandLinkItem = createComponent(function CommandLinkItem(
{ class: className, ...props }: CommandLinkItemProps,
...children: Child[]
) {
return CommandLinkItemPrimitive(
{ ...props, "data-slot": "command-link-item", class: cn(itemClass, className) },
...children,
);
});
export const CommandSeparator = createComponent(function CommandSeparator(
{ class: className, ...props }: CommandSeparatorProps,
...children: Child[]
) {
return CommandSeparatorPrimitive(
{ ...props, "data-slot": "command-separator", class: cn("h-px bg-border", className) },
...children,
);
});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
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.
| 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. 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 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. Styled with a 20rem cap and its own vertical scrolling. 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 |