Command
A searchable list of actions — the embeddable form of a command palette, which CommandDialog wraps into the ⌘K overlay most people picture.
Usage#
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "neelam-ui";
<Command>
<CommandInput placeholder="Type a command or search…" />
<CommandList>
<CommandGroup heading="Suggestions">
<CommandItem value="calendar" onSelect={run}>Calendar</CommandItem>
</CommandGroup>
<CommandEmpty>No results found.</CommandEmpty>
</CommandList>
</Command>Command on its own works planted directly in a page — a docs site's search
box, for instance. It is not tied to a modal.
As a ⌘K dialog#
CommandDialog wraps the same parts in a Dialog:
const [open, setOpen] = useState(false);
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command…" />
<CommandList>…</CommandList>
</CommandDialog>Its title is the dialog's accessible name and is visually hidden by default
("Command palette"), since a palette jumps straight to its input rather than
showing a heading. Binding the keyboard shortcut is yours to do — the component
does not install a global listener on your behalf.
Item labels must be plain strings#
CommandItem's children is typed as string, not arbitrary nodes. Filtering
needs an item's text before deciding whether to render it at all, so the label
has to be knowable up front.
Mixing an icon into children crashes
This is not a style rule. Filtering short-circuits past .toLowerCase() while
the query is still empty, so a non-string child does not fail on mount — it
fails the moment someone actually types. Use the icon prop, which exists for
exactly this.
<CommandItem value="profile" icon={<User className="h-4 w-4" aria-hidden="true" />}>
Profile
</CommandItem>value identifies the item, is what the query matches against, and is what
onSelect receives.
Empty and filtered states#
A non-matching CommandItem renders null entirely, not merely hidden.
CommandGroup hides itself — heading included — once every item inside it has
been filtered out, rather than leaving a heading floating over nothing.
CommandEmpty renders only when nothing at all matches; include it, or a
fruitless search collapses into a blank box.
Keyboard#
| Key | Behaviour |
|---|---|
| A–Z | Filters the list. Focus never leaves the input. |
| ↓↑ | Moves the highlight through the visible results. |
| Enter | Runs the highlighted item's onSelect. |
| Escape | Closes the palette, when used inside CommandDialog. |
Accessibility#
CommandInputis a native<input>witharia-autocomplete="list"andaria-controls. It is deliberately notrole="combobox": there is no persistent selected value here the way there is inCombobox, only a live filter.- The highlight is tracked with
aria-activedescendant, so real focus stays in the input and the user can keep typing to refine the search. CommandListisrole="listbox"and defaultsaria-labelto "Results" — a listbox takes no name from its content, so without it axe reports no accessible name at all. Override it for something more specific.- When filtering leaves nothing visible, the
listboxrole is dropped altogether: a listbox whose only child is an empty-state message is anaria-required-childrenviolation, andaria-activedescendantwould have nothing to point at. CommandGroupis labelled by its own heading viaaria-labelledby.
API reference#
Command#
| Prop | Type | Default |
|---|---|---|
defaultOpenInitial open state when uncontrolled. Defaults to `false`. | boolean | — |
onOpenChangeCalled whenever the open state changes, whether from `DialogTrigger`, `DialogClose`, the built-in close button, Escape, or an outside click. | ((open: boolean) => void) | — |
openControls the open state. Omit to let the dialog manage its own state. | boolean | — |
titleThe accessible name for the dialog — visually hidden, since a command palette jumps straight to its search input rather than showing a visible heading the way `Dialog` normally does. Defaults to `"Command palette"`. | ReactNode | Command palette |
CommandDialog#
| Prop | Type | Default |
|---|---|---|
children | ReactNode | — |
defaultOpenInitial open state when uncontrolled. Defaults to `false`. | boolean | — |
onOpenChangeCalled whenever the open state changes, whether from `DialogTrigger`, `DialogClose`, the built-in close button, Escape, or an outside click. | ((open: boolean) => void) | — |
openControls the open state. Omit to let the dialog manage its own state. | boolean | — |
titleThe accessible name for the dialog — visually hidden, since a command palette jumps straight to its search input rather than showing a visible heading the way `Dialog` normally does. Defaults to `"Command palette"`. | ReactNode | "Command palette" |
CommandGroup#
| Prop | Type | Default |
|---|---|---|
headingrequired | string | — |
CommandItem#
| Prop | Type | Default |
|---|---|---|
childrenrequiredThe visible label — a plain string, not arbitrary `children`, the same constraint `ComboboxItem` has and for the same reason: filtering needs an item's text before deciding whether to render it at all. Mixing an icon element in here (rather than using `icon` below) isn't just a style choice this rules out — it's a real crash, caught directly against this component's own first "as a command palette" story: filtering short-circuits past `.toLowerCase()` while the query is still empty, so it doesn't fail immediately, only the moment someone actually types something. | string | — |
valuerequiredIdentifies this item — matched against the query, and passed to `onSelect`. | string | — |
iconAn icon shown before the label — the place for one, since `children` can't hold anything but the label text. | ReactNode | — |
onSelectCalled when this item is activated (clicked, or Enter while it's highlighted). Named to match `value`'s own vocabulary, not the native `onSelect` (a text-selection event every `HTMLAttributes` element technically has) it shadows — `Omit`ted from the base props above so this one, with its own unrelated signature, can take its place. | ((value: string) => void) | — |