Skip to content
design
Components

Command palette

A searchable dialog gives direct access to named destinations and actions with keyboard or pointer selection.

On this page

No command selected.

Find a destination or action by name, description or keyword, then explicitly select it.
Code
import { Button } from "@bosunkit/components/button";
import { CommandPalette, type CommandGroup, type CommandItem } from "@bosunkit/components/command-palette";
import { Kbd } from "@bosunkit/components/kbd";
import { BookOpen, FileText, HelpCircle, Settings, Users } from "lucide-react";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
  "Search names and keywords, move with the arrow keys, and choose with Enter or a press. Selection is reported locally; the example performs no navigation or writes.";
const groups: readonly CommandGroup[] = [
  {
    id: "pages",
    label: "Destinations",
    items: [
      {
        id: "reports",
        label: "Open reports",
        description: "Browse the report library",
        keywords: ["updates", "engineering"],
        icon: <FileText />,
        shortcut: "G R",
      },
      {
        id: "crew",
        label: "Open crew directory",
        description: "People and contact details",
        keywords: ["team", "people"],
        icon: <Users />,
        shortcut: "G C",
      },
      { id: "design", label: "Open design foundations", description: "Type, spacing and themes", icon: <BookOpen /> },
    ],
  },
  {
    id: "tools",
    label: "Tools",
    items: [
      { id: "settings", label: "Open workspace settings", icon: <Settings /> },
      {
        id: "access",
        label: "Manage publishing access",
        description: "Unavailable in this read-only example",
        disabled: true,
        icon: <Users />,
      },
      { id: "help", label: "Show keyboard help", icon: <HelpCircle /> },
    ],
  },
];
export default function Showcase(): ReactElement {
  const [open, setOpen]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(false);
  const [query, setQuery]: [string, Dispatch<SetStateAction<string>>] = useState("");
  const [chosen, setChosen]: [string, Dispatch<SetStateAction<string>>] = useState("No command selected.");
  return (
    <div style={{ display: "grid", gap: "var(--space-md)" }}>
      <div>
        <Button
          onClick={(): void => {
            setQuery("");
            setOpen(true);
          }}
        >
          Find a destination or action
        </Button>
      </div>
      <p role="status">{chosen}</p>
      <CommandPalette
        open={open}
        onOpenChange={setOpen}
        title="Go to or do something"
        description="Choose from destinations and workspace tools."
        closeLabel="Close command menu"
        searchLabel="Search commands"
        clearLabel="Clear command search"
        placeholder="Search pages, actions or keywords…"
        emptyText="No commands match. Try another word."
        loadingText="Loading commands…"
        groups={groups}
        query={query}
        onQueryChange={setQuery}
        onSelect={(item: CommandItem): void => {
          setChosen(`Selected: ${item.label}. This preview does not run the action.`);
        }}
        footer={
          <span>
            <Kbd>↑</Kbd> <Kbd>↓</Kbd> Move · <Kbd>Enter</Kbd> Choose · <Kbd>Esc</Kbd> Close
          </span>
        }
      />
    </div>
  );
}

The command palette combines the scoped task dialog with Base UI's inline autocomplete list. The search field receives focus on opening. Highlighting a result never executes it: Enter or a pointer press calls onSelect, and the default behavior requests closing. The caller owns routing and actions.

01Supplement visible navigationKeep essential destinations and actions discoverable elsewhere. A command menu provides another route to them.
02Execute only on selectionTyping, highlighting and focus do not perform an action. Name commands by what choosing them will do.
03Keep context and availability clearGroup related commands, explain unavailable actions, and show loading or empty results in words.

Examples

Drill down in one dialog

Set closeOnSelect={false} to replace the current groups after a choice. Keep a visible route back and reset the query when changing the command collection. Avoid stacking another command dialog over the first.

No template selected.

Choosing a report type opens a second command group in the same dialog. Back restores the first group, and choosing a template closes the example.
Code
import { Button } from "@bosunkit/components/button";
import { CommandPalette, type CommandGroup, type CommandItem } from "@bosunkit/components/command-palette";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
  "Choosing a report type opens a second command group in the same dialog. Back restores the first group, and choosing a template closes the example.";
const root: readonly CommandGroup[] = [
  {
    id: "types",
    label: "Report types",
    items: [
      { id: "daily", label: "Daily commit report", description: "Open its available templates" },
      { id: "technical", label: "Technical report", description: "Open its available templates" },
    ],
  },
];
export default function DrillDown(): ReactElement {
  const [open, setOpen]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(false);
  const [kind, setKind]: [string, Dispatch<SetStateAction<string>>] = useState("");
  const [query, setQuery]: [string, Dispatch<SetStateAction<string>>] = useState("");
  const [chosen, setChosen]: [string, Dispatch<SetStateAction<string>>] = useState("No template selected.");
  const groups: readonly CommandGroup[] =
    kind === ""
      ? root
      : [
          {
            id: "templates",
            label: kind,
            items: [
              { id: "concise", label: "Concise update", description: "Summary, evidence and next actions" },
              { id: "detailed", label: "Detailed review", description: "Methods, findings, limits and references" },
            ],
          },
        ];
  return (
    <div style={{ display: "grid", gap: "var(--space-md)" }}>
      <div>
        <Button
          onClick={(): void => {
            setKind("");
            setQuery("");
            setOpen(true);
          }}
        >
          Choose a report template
        </Button>
      </div>
      <p role="status">{chosen}</p>
      <CommandPalette
        open={open}
        onOpenChange={setOpen}
        title={kind === "" ? "Choose report type" : "Choose template"}
        closeLabel="Close template chooser"
        searchLabel="Find a template"
        clearLabel="Clear template search"
        emptyText="No matching templates."
        loadingText="Loading templates…"
        groups={groups}
        query={query}
        onQueryChange={setQuery}
        closeOnSelect={false}
        onSelect={(item: CommandItem): void => {
          if (kind === "") {
            setKind(item.label);
            setQuery("");
          } else {
            setChosen(`${kind}: ${item.label}`);
            setOpen(false);
          }
        }}
        footer={
          kind === "" ? (
            "Choose a type to see its templates."
          ) : (
            <Button
              variant="ghost"
              onClick={(): void => {
                setKind("");
                setQuery("");
              }}
            >
              Back to report types
            </Button>
          )
        }
      />
    </div>
  );
}

Loading and empty results

busy hides stale options and prevents selection while keeping the input editable. The caller supplies the loading message and controls the result set. These controls simulate states locally.

No selection.

Simulate caller-owned loading and empty results. A loading menu keeps the query editable and blocks commands until results are supplied; no network request is made.
Code
import { Button } from "@bosunkit/components/button";
import { CommandPalette, type CommandGroup, type CommandItem } from "@bosunkit/components/command-palette";
import { Switch } from "@bosunkit/components/switch";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
  "Simulate caller-owned loading and empty results. A loading menu keeps the query editable and blocks commands until results are supplied; no network request is made.";
const groups: readonly CommandGroup[] = [
  { id: "results", label: "Available results", items: [{ id: "draft", label: "Open latest draft" }] },
];
export default function Loading(): ReactElement {
  const [open, setOpen]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(false);
  const [busy, setBusy]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(true);
  const [empty, setEmpty]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(false);
  const [selected, setSelected]: [string, Dispatch<SetStateAction<string>>] = useState("No selection.");
  return (
    <div style={{ display: "grid", gap: "var(--space-md)" }}>
      <div>
        <Button
          onClick={(): void => {
            setOpen(true);
          }}
        >
          Preview loading and empty states
        </Button>
      </div>
      <p role="status">{selected}</p>
      <CommandPalette
        open={open}
        onOpenChange={setOpen}
        title="Find a draft"
        closeLabel="Close draft search"
        searchLabel="Search drafts"
        clearLabel="Clear draft search"
        emptyText="No draft results. Try another query."
        loadingText="Loading draft results…"
        groups={empty ? [] : groups}
        busy={busy}
        onSelect={(item: CommandItem): void => {
          setSelected(item.label);
        }}
        footer={
          <div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-lg)" }}>
            <label style={{ display: "flex", alignItems: "center", gap: "var(--space-sm)" }}>
              <Switch checked={busy} onCheckedChange={setBusy} />
              Simulate loading
            </label>
            <label style={{ display: "flex", alignItems: "center", gap: "var(--space-sm)" }}>
              <Switch checked={empty} onCheckedChange={setEmpty} />
              Empty result set
            </label>
          </div>
        }
      />
    </div>
  );
}

Search and result ownership

The default filter matches every query word against the label, description and keywords, ignoring case. It keeps the supplied order and groups, dropping groups with no matches. It does not rank results, perform fuzzy matching or load remote data. Supply a filter function for another local policy, or filter={null} for results that have already been filtered by the caller.

query, defaultQuery and onQueryChange support controlled and local search text. Local text is retained across closing and reopening; set a controlled query when opening should start fresh. Use stable, globally unique command ids and stable group ids. A display label is not an identifier.

For remote results, the caller debounces, aborts superseded requests and rejects stale responses. Use status for a translated count or failure explanation, and offer retry through the footer when needed. Only accepted results should be supplied as available commands. Permissions and command authorization remain server-owned.

Keyboard and dismissal

Arrow keys move the active result, Enter chooses it, and Escape dismisses the dialog. Disabled results remain visible with their descriptions but cannot be chosen. Tab reaches controls such as Clear and the footer while staying inside the dialog. Focus returns to the opener, or to finalFocus when that opener disappears.

The footer can explain these keys with Kbd. An item's shortcut is a displayed hint, not a registered listener. Register application shortcuts once in the shell, clean them up on unmount, respect composition and editable fields, and coordinate with other overlays. The design site's own search shortcut is owned by its shell, so the specimens open through visible buttons.

Actions and navigation

A command is an explicit choice in a listbox, not a normal hyperlink. Route through the application in onSelect, and preserve real links in the main navigation for opening destinations in another tab. Destructive or consequential actions should open their own review/confirmation flow. Never run them because their row is highlighted or a query matches their name.

Appearance and scope

The dialog uses top placement, the shared medium or large width, and a bounded result list. Its input, results, status and footer use interface typography. Theme, size class and language cross the portal through Modal; keyboard hints and icons use the existing kit components. Long descriptions wrap rather than hiding context.

Props

PropTypeDefaultDescription
openrequiredboolean–Whether the command dialog is open.
onOpenChangerequired(open: boolean) => void–Requests opening or dismissal; the caller owns any global shortcut.
titlerequiredstring–Visible dialog title.
descriptionstring | undefined–Optional brief dialog description.
closeLabelrequiredstring–Name of the visible close button.
searchLabelrequiredstring–Accessible name of the search field and results.
clearLabelrequiredstring–Name of the search clear control.
emptyTextrequiredstring–Text shown for no matching commands.
loadingTextrequiredstring–Text shown and announced while busy.
groupsrequiredreadonly CommandGroup[]–Grouped commands; the caller owns routing, permission checks and persistence.
onSelectrequired(item: CommandItem) => void–Called on explicit keyboard or pointer selection. No command runs on focus or highlight.
querystring | undefined–Controlled search text.
defaultQuerystring | undefined""Initial search text in uncontrolled use; retained across closing and reopening.
onQueryChange((query: string) => void) | undefined–Reports typing and clearing, for controlled filtering or caller-owned remote loading.
filter((item: CommandItem, query: string) => boolean) | null | undefinedmatchesCustom filter; null means the supplied commands are already filtered.
busyboolean | undefinedfalseBlocks selection and shows loadingText while the caller loads commands.
statusReactNode | undefined–A translated result summary, failure explanation or other polite status.
placeholderstring | undefined–Text inside an empty search field.
footerReactNode | undefined–Supporting footer, for example keyboard instructions using Kbd.
closeOnSelectboolean | undefinedtrueRequest closing after selection; true by default. False supports drill-down menus.
size"md" | "lg" | undefined"md"Dialog width; md by default.
motionboolean | undefinedtrueWhether the dialog moves as a TaskDialog does: on by default; false opens and closes it at once.
finalFocusRefObject<HTMLElement | null> | undefined–Focus destination when the opener no longer exists.
classNamestring | undefined–Additional dialog classes.
testIdstring | undefined–Dialog test id, with TaskDialog's part suffixes.
refRef<HTMLDivElement> | undefined–The dialog content element.
Do

Open report library · Create a local draft

Name the outcome and wait for explicit selection.
Don't

Typing a name immediately opens another page.

Do not execute a matching command while the user is typing.
  • Navigation: the complete navigation family.
  • Task dialog: focus, dismissal and portal presentation.
  • Search input: queries and collection search.
  • Kbd: visible keyboard instructions.

Implementation references: Base UI autocomplete and WAI-ARIA combobox pattern.