Command
commandSearches and executes grouped commands through a keyboard-first palette.
Usage
Basic usage
Command is in controlled mode: hold open with useState, and the button triggers to open. groups provides grouping commands, and the input box is filtered across groups in real time (try entering "Order", "dd" and "Topic").
const [open, setOpen] = useState(false);
const groups = [
{
heading: "Quick jump",
items: [
{ value: "go-dashboard", label: "Dashboard", description: "Overview of today's data",
keywords: "dashboard Home", icon: <LayoutDashboard /> },
{ value: "go-orders", label: "Order Management", keywords: "order Order dd",
icon: <ShoppingCart /> },
],
},
{
heading: "Operation",
items: [
{ value: "new-order", label: "New Order", icon: <Plus />, shortcut: "⌘N" },
{ value: "import", label: "Import data", description: "Upload CSV / Excel",
icon: <Upload /> },
],
},
];
<Button variant="outline" onClick={() => setOpen(true)}>Open the command panel</Button>
<Command open={open} onOpenChange={setOpen} groups={groups} />Built-in ⌘K shortcut keys
After shortcut is turned on, the component has built-in ⌘K / Ctrl+K global monitoring switch, no need to bind it yourself.
<Button variant="outline" onClick={() => setOpen(true)}>
Open the command panel (or ⌘K)
</Button>
<Command open={open} onOpenChange={setOpen} groups={groups} shortcut />Keep open after selection
closeOnSelect={false} The panel does not close after executing the command, which is suitable for scenarios with multiple consecutive operations.
<Command
open={open}
onOpenChange={setOpen}
groups={groups}
closeOnSelect={false}
/>Pinned footer
footer renders outside the list, so it neither scrolls with the list nor disappears when filtering empties the results. Use it for the switch that decides what this selection means, plus a hint in the bottom-right corner: the palette is modal, so these controls have nowhere else to go.
const [mode, setMode] = useState("related");
<Command
open={open}
onOpenChange={setOpen}
groups={groups}
placeholder="Search tasks…"
footer={
<div className="flex items-center justify-between gap-2">
<Segmented
size="sm"
value={mode}
onValueChange={setMode}
items={[
{ value: "related", label: "Related" },
{ value: "blocks", label: "Blocks" },
]}
/>
<span className="text-xs text-muted-foreground">Pick a task</span>
</div>
}
/>When to use
Use Command for a global Command/Ctrl+K palette that brings cross-page navigation, actions, and theme switching into one searchable, grouped, keyboard-navigable surface. Use Toolbar for a persistent row of controls or ContextMenu for actions opened at a pointer location. Command is a modal, data-driven search entry point.
Import
import { Command, useCommandShortcut } from "@hulianui/ui"Props
| Name | Type | Default | Description |
|---|---|---|---|
| open* | boolean | - | Controlled open state. |
| groups* | CommandGroupData[] | - | Command groups, each with an optional heading. |
| placeholder | string | "\u8f93\u5165\u547d\u4ee4\u6216\u641c\u7d22\u2026" | Search-field placeholder. The built-in Chinese copy means “Type a command or search…”. |
| filter | (item: CommandItemData, query: string) => boolean | Substring match | Custom predicate; return true to retain an item. The default case-insensitively searches keywords, string label, and value. |
| closeOnSelect | boolean | true | Whether executing an item closes the palette. |
| autoHighlight | boolean | true | Whether opening the palette and every filter pass highlight the first enabled item, so that typing and pressing Enter hits it directly. Turn it off and an arrow key must light an item up before Enter does anything. |
| shortcut | boolean | false | Whether to install the global Command/Ctrl+K open-state shortcut. |
| surface | "solid" | "glass" | "none" | "solid" | Surface skin of the panel shell, covering only fill, border, and shadow; size and position always stay with the component. glass is translucent with a backdrop blur and needs artwork behind it; none draws no skin classes at all and hands fill, border, and shadow to className. |
| className | string | - | Additional class name for the panel shell. |
| backdropClassName | string | - | Appended to the backdrop, whose default is bg-black/40 backdrop-blur-sm. Classes merge with twMerge, so the dimming and blur can follow your own design system. |
| aria-label | string | "\u547d\u4ee4\u9762\u677f" | Accessible label. The built-in Chinese copy means “Command palette.” |
Events
| Event | Type | Description |
|---|---|---|
| onOpenChange* | (open: boolean) => void | Called when the palette requests an open-state change. |
| onSelectItem | (value: string) => void | Called after an item's own onSelect, with its value. |
| onQueryChange | (query: string) => void | Called on query changes, including the reset each time the palette opens. Use it with filter={() => true} when the consumer owns sorting, grouping, and filtering. |
Slots
| Slot | Type | Description |
|---|---|---|
| emptyMessage | ReactNode | Empty-state content shown when no command matches. The built-in Chinese copy is "\u65e0\u5339\u914d\u7ed3\u679c", meaning “No matching results.” |
| footer | ReactNode | A footer pinned below the list (mode switch, hint, count). It sits outside the list, so it neither scrolls with the list nor disappears when filtering empties the results. |
footer: the pinned row at the bottom of the panel
The palette is modal, so controls in the footer have nowhere else to go. A switch that decides what this selection means (link versus block, for example) turns into a step before the search once it moves to the trigger, and stays on the page even while the palette is closed once it moves to a section header. Put it in footer so the user can search first and decide after:
<Command
open={open}
onOpenChange={setOpen}
groups={groups}
footer={
<div className="flex items-center justify-between gap-2">
<Segmented value={mode} onValueChange={setMode} items={[{ value: "related", label: "Related" }, { value: "blocks", label: "Blocks" }]} />
<span className="text-xs text-muted-foreground">Pick a task</span>
</div>
}
/>The component supplies only the frame: a top separator, padding, and the panel's own text-sm size. Layout and color inside the footer belong to its content, matching ComboboxContent's footer.
CommandGroupData: heading?: ReactNode / items: CommandItemData[].
CommandItemData
| Field | Type | Description |
|---|---|---|
| value* | string | Unique value used for callbacks, the React key, and fallback filtering. |
| label* | ReactNode | Visible title. |
| keywords | string | Additional searchable terms, especially for non-string labels. |
| description | ReactNode | Muted secondary content below the label. |
| icon | ReactNode | Leading icon. |
| shortcut | ReactNode | Trailing keyboard shortcut or marker. |
| disabled | boolean | Whether the item is unavailable. |
| onSelect | (value: string) => void | Called when Enter or a click executes the item. |
Example
const groups = [
{
heading: "Quick navigation",
items: [
{ value: "go-orders", label: "Orders", keywords: "orders", icon: <ShoppingCart />, onSelect: () => router.push("/orders") },
{ value: "new-order", label: "New order", icon: <Plus />, shortcut: "⌘N" },
],
},
];
const [open, setOpen] = useState(false);
<Command open={open} onOpenChange={setOpen} groups={groups} shortcut placeholder="Type a command or search…" />Usage guidelines
- Command is always controlled; provide both
openandonOpenChange. - A non-string
labelis not directly searchable by the default filter. Addkeywords, or the item can match only throughvalue. - Enable the built-in shortcut with
shortcut. If the surrounding application installs its own trigger, useuseCommandShortcutinstead and do not enable both. - Consider
autoHighlight={false}when the commands are destructive, such as delete, reset, or publish: the default "highlight the first item on open" plus a stray Enter is a misfire. With it off, the user has to light an item up with an arrow key before Enter does anything. - Prefer
surface="none"plus your own classes over usingclassNameto override thesolidskin (bg-surface,border-hairline,shadow-xl): overrides fight future skin changes, and swapping a background color would otherwise force the layout classes through twMerge as well. surface="glass"only reads as glass when there is artwork behind the panel; on a flat page it is just a translucent panel. Backdrop dimming is a separate knob,backdropClassName.- The highlight follows the item's `value`, not the array reference: an item that survives filtering keeps its highlight, so rebuilding
groupson every render does not make the highlight jump. In return,valuemust be stable: never derive it from the array index, or every batch of results looks like a set of new items.
Related
ContextMenu · Toolbar · Accordion · Collapsible · Link · AnimatedThemeToggler
Playground
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open the command panel</Button>
<Command
open={open}
onOpenChange={setOpen}
placeholder="Enter command or search..."
shortcut={false}
groups={[
{ heading: "Commonly used", items: [{ value: "new", label: "New file", onSelect: (v) => {} }] },
]}
/>