Siteplane UI

Command

Find and run actions in an inline list or a dialog.

Markdown
"use client";

import {
  ArrowDownIcon,
  ArrowUpIcon,
  CornerDownLeftIcon,
  CreditCardIcon,
  FolderIcon,
  FolderPlusIcon,
  LayoutDashboardIcon,
  type LucideIcon,
  SettingsIcon,
  UserPlusIcon,
  UsersIcon,
} from "lucide-react";
import { Fragment, useEffect, useState } from "react";
import { Button } from "@/siteplane/components/button";
import {
  Command,
  CommandCollection,
  CommandDialog,
  CommandDialogPopup,
  CommandDialogTrigger,
  CommandEmpty,
  CommandFooter,
  CommandGroup,
  CommandGroupLabel,
  CommandInput,
  CommandItem,
  CommandList,
  CommandPanel,
  CommandSeparator,
  CommandShortcut,
} from "@/siteplane/components/command";
import { Kbd, KbdGroup } from "@/siteplane/components/kbd";

type CommandAction = { icon: LucideIcon; label: string; shortcut?: string };

const groups: { value: string; items: CommandAction[] }[] = [
  {
    value: "Pages",
    items: [
      { icon: LayoutDashboardIcon, label: "Dashboard" },
      { icon: FolderIcon, label: "Projects" },
      { icon: UsersIcon, label: "Members" },
      { icon: CreditCardIcon, label: "Billing" },
    ],
  },
  {
    value: "Actions",
    items: [
      { icon: FolderPlusIcon, label: "Create project", shortcut: "⌘N" },
      { icon: UserPlusIcon, label: "Invite member", shortcut: "⌘I" },
      { icon: SettingsIcon, label: "Open settings", shortcut: "⌘," },
    ],
  },
];

export default function Example() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    function onKeyDown(event: KeyboardEvent) {
      if (event.key === "j" && (event.metaKey || event.ctrlKey)) {
        event.preventDefault();
        setOpen((current) => !current);
      }
    }
    document.addEventListener("keydown", onKeyDown);
    return () => document.removeEventListener("keydown", onKeyDown);
  }, []);

  return (
    <CommandDialog onOpenChange={setOpen} open={open}>
      <CommandDialogTrigger render={<Button variant="outline" />}>
        Open command palette
        <KbdGroup>
          <Kbd>⌘</Kbd>
          <Kbd>J</Kbd>
        </KbdGroup>
      </CommandDialogTrigger>
      <CommandDialogPopup>
        <Command
          items={groups}
          itemToStringValue={(item: CommandAction) => item.label}
        >
          <CommandInput placeholder="Type a command or search…" />
          <CommandPanel>
            <CommandEmpty>No results found.</CommandEmpty>
            <CommandList>
              {(group: (typeof groups)[number], index: number) => (
                <Fragment key={group.value}>
                  <CommandGroup items={group.items}>
                    <CommandGroupLabel>{group.value}</CommandGroupLabel>
                    <CommandCollection>
                      {(item: CommandAction) => (
                        <CommandItem
                          key={item.label}
                          onClick={() => setOpen(false)}
                          value={item}
                        >
                          <item.icon />
                          <span className="flex-1">{item.label}</span>
                          {item.shortcut && (
                            <CommandShortcut>{item.shortcut}</CommandShortcut>
                          )}
                        </CommandItem>
                      )}
                    </CommandCollection>
                  </CommandGroup>
                  {index < groups.length - 1 && <CommandSeparator />}
                </Fragment>
              )}
            </CommandList>
          </CommandPanel>
          <CommandFooter>
            <div className="flex items-center gap-3">
              <div className="flex items-center gap-1.5">
                <KbdGroup>
                  <Kbd>
                    <ArrowUpIcon />
                  </Kbd>
                  <Kbd>
                    <ArrowDownIcon />
                  </Kbd>
                </KbdGroup>
                <span>Navigate</span>
              </div>
              <div className="flex items-center gap-1.5">
                <Kbd>
                  <CornerDownLeftIcon />
                </Kbd>
                <span>Open</span>
              </div>
            </div>
            <div className="flex items-center gap-1.5">
              <Kbd>Esc</Kbd>
              <span>Close</span>
            </div>
          </CommandFooter>
        </Command>
      </CommandDialogPopup>
    </CommandDialog>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/command

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/kbd

Usage

import { SettingsIcon } from "lucide-react";
import {
  Command,
  CommandInput,
  CommandPanel,
  CommandList,
  CommandItem,
} from "@/siteplane/components/command";

<Command items={["Settings"]}>
  <CommandInput aria-label="Search" />
  <CommandPanel>
    <CommandList>
      {(item: string) => (
        <CommandItem value={item}>
          <SettingsIcon />
          {item}
        </CommandItem>
      )}
    </CommandList>
  </CommandPanel>
</Command>;

Composition

Command
├── CommandInput
├── CommandPanel
│   ├── CommandEmpty
│   └── CommandList → CommandItem
│       or CommandGroup → CommandGroupLabel + CommandCollection → CommandItem
└── CommandFooter

CommandDialog → CommandDialogTrigger + CommandDialogPopup → Command

Inline command

Command renders an open, inline list by default, so it can sit in a page or panel without a dialog. CommandShortcut only displays a hint; register the matching keyboard handler in your application.

The search row and the results follow the control size: the row is 40, 44, 48 and 60 px tall and a result 37, 41, 45 and 56 px from sm to xl. A size on CommandInput sizes only its text.

"use client";

import {
  CreditCardIcon,
  FolderPlusIcon,
  SettingsIcon,
  UserIcon,
  UserPlusIcon,
} from "lucide-react";
import { Fragment } from "react";
import {
  Command,
  CommandCollection,
  CommandEmpty,
  CommandGroup,
  CommandGroupLabel,
  CommandInput,
  CommandItem,
  CommandList,
  CommandPanel,
  CommandSeparator,
  CommandShortcut,
} from "@/siteplane/components/command";

type CommandAction = {
  icon: typeof UserIcon;
  label: string;
  shortcut?: string;
};

const groups: { value: string; items: CommandAction[] }[] = [
  {
    value: "Suggestions",
    items: [
      { icon: FolderPlusIcon, label: "Create project" },
      { icon: UserPlusIcon, label: "Invite member" },
    ],
  },
  {
    value: "Settings",
    items: [
      { icon: UserIcon, label: "Profile", shortcut: "⌘P" },
      { icon: CreditCardIcon, label: "Billing", shortcut: "⌘B" },
      { icon: SettingsIcon, label: "Settings", shortcut: "⌘S" },
    ],
  },
];

export default function Example() {
  return (
    <div className="w-full max-w-md">
      <Command
        items={groups}
        itemToStringValue={(item: CommandAction) => item.label}
      >
        <CommandInput placeholder="Type a command or search…" />
        <CommandPanel>
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandList>
            {(group: (typeof groups)[number], index: number) => (
              <Fragment key={group.value}>
                <CommandGroup items={group.items}>
                  <CommandGroupLabel>{group.value}</CommandGroupLabel>
                  <CommandCollection>
                    {(item: CommandAction) => (
                      <CommandItem key={item.label} value={item}>
                        <item.icon />
                        <span className="flex-1">{item.label}</span>
                        {item.shortcut && (
                          <CommandShortcut>{item.shortcut}</CommandShortcut>
                        )}
                      </CommandItem>
                    )}
                  </CommandCollection>
                </CommandGroup>
                {index < groups.length - 1 && <CommandSeparator />}
              </Fragment>
            )}
          </CommandList>
        </CommandPanel>
      </Command>
    </div>
  );
}

API reference

Component properties
PropTypeDefaultDescription
Command.itemsreadonly ItemValue[]—String/object items or grouped collections.
Command.autoHighlightAutocomplete autoHighlightalwaysHighlights a matching item automatically.
Command.keepHighlightbooleantrueKeeps the highlight across pointer movement.
Command inline / openAutocomplete root propstrue / trueInline, visible command list by default.
CommandInputAutocompleteInput propsaria-label "Search commands", no placeholderSearch input; override aria-label to describe what it searches.
CommandPaneldiv props—Scroll/layout container.
CommandList / CommandEmptyAutocompleteList / AutocompleteEmpty props—Filtered render callback and empty result.
CommandGroup / CommandGroupLabel / CommandCollectionMatching Autocomplete part props—Grouped item rendering.
CommandItemAutocompleteItem props—value, disabled, onClick and render.
CommandSeparatorAutocompleteSeparator props—Decorative division.
CommandShortcut / CommandFooterkbd / div props—Shortcut presentation and supporting instructions.
CommandDialogPopupDialogPopup propsaria-label "Command palette", no close buttonDialog popup sized for a command palette.
CommandDialog / CommandDialogTrigger / CommandDialogPortal / CommandDialogBackdrop / CommandDialogViewportCorresponding Dialog partsDialog defaultsAliases for dialog composition.
CommandCreateHandle / CommandDialogPrimitiveDialogCreateHandle / DialogPrimitive—Detached handle and foundation namespace.

Read the installed source.

Usage notes

Remaining root and item contracts follow Autocomplete; dialog state, focus, portals and dismissal follow Dialog. This component does not register global shortcuts, execute commands, persist history, or navigate automatically; the command palette example adds its own ⌘J/Ctrl+J handler.