Skip to content

Documentation

neelam-ui

Search documentation

Docs
Components
Blocks
GitHub repository

Command

A searchable list of actions — the embeddable form of a command palette, which CommandDialog wraps into the ⌘K overlay most people picture.

Suggestions
Settings

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#

Keyboard shortcuts
KeyBehaviour
A–ZFilters the list. Focus never leaves the input.
↓↑Moves the highlight through the visible results.
EnterRuns the highlighted item's onSelect.
EscapeCloses the palette, when used inside CommandDialog.

Accessibility#

  • CommandInput is a native <input> with aria-autocomplete="list" and aria-controls. It is deliberately not role="combobox": there is no persistent selected value here the way there is in Combobox, 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.
  • CommandList is role="listbox" and defaults aria-label to "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 listbox role is dropped altogether: a listbox whose only child is an empty-state message is an aria-required-children violation, and aria-activedescendant would have nothing to point at.
  • CommandGroup is labelled by its own heading via aria-labelledby.

API reference#

Command#

Props for Command
PropTypeRequiredDefault
defaultOpen

Initial open state when uncontrolled. Defaults to `false`.

boolean——
onOpenChange

Called whenever the open state changes, whether from `DialogTrigger`, `DialogClose`, the built-in close button, Escape, or an outside click.

((open: boolean) => void)——
open

Controls the open state. Omit to let the dialog manage its own state.

boolean——
title

The 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#

Props for CommandDialog
PropTypeRequiredDefault
childrenReactNode——
defaultOpen

Initial open state when uncontrolled. Defaults to `false`.

boolean——
onOpenChange

Called whenever the open state changes, whether from `DialogTrigger`, `DialogClose`, the built-in close button, Escape, or an outside click.

((open: boolean) => void)——
open

Controls the open state. Omit to let the dialog manage its own state.

boolean——
title

The 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#

Props for CommandGroup
PropTypeRequiredDefault
headingstringYes—

CommandItem#

Props for CommandItem
PropTypeRequiredDefault
children

The 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.

stringYes—
value

Identifies this item — matched against the query, and passed to `onSelect`.

stringYes—
icon

An icon shown before the label — the place for one, since `children` can't hold anything but the label text.

ReactNode——
onSelect

Called 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)——