Skip to content
Dethink Components

ComponentsCommandPalette

CommandPalette

Let users search for actions and pages from one place.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

Import the component into your page or component file.

Usage
import {
  CommandPalette,
  CommandPaletteContent,
  CommandPaletteDialog,
  CommandPaletteTrigger,
  type CommandPaletteCommand,
} from "@dethink/components";

CommandPalette can render directly from typed data or be composed from anatomy primitives when a row needs app-owned markup.

Basic

Grouped action and link commands with shortcuts, disabled reasons, and destructive state.

Search actions, links, and guarded destructive commands.

Actions
Danger
5 commands available.
Show sourceexamples/command-palette/basic.tsx
examples/command-palette/basic.tsx
"use client";

import {
  CommandPalette,
  type CommandPaletteCommand,
} from "@dethink/components";
import {
  Archive,
  ExternalLink,
  Settings2,
  Trash2,
  UserPlus,
} from "lucide-react";

const commands: CommandPaletteCommand[] = [
  {
    description: "Send an invite to this workspace",
    group: "Actions",
    icon: <UserPlus aria-hidden="true" className="size-4" />,
    key: "invite",
    keywords: ["people", "member"],
    label: "Invite teammate",
    shortcut: "G I",
  },
  {
    description: "Move the current project to the archive",
    group: "Actions",
    icon: <Archive aria-hidden="true" className="size-4" />,
    key: "archive-project",
    label: "Archive project",
  },
  {
    group: "Navigation",
    href: "#settings",
    icon: <Settings2 aria-hidden="true" className="size-4" />,
    key: "settings",
    label: "Open settings",
    shortcut: "G S",
    type: "link",
  },
  {
    group: "Navigation",
    href: "https://dethink.dev",
    icon: <ExternalLink aria-hidden="true" className="size-4" />,
    key: "docs",
    label: "Open docs",
    target: "_blank",
    type: "link",
  },
  {
    group: "Danger",
    icon: <Trash2 aria-hidden="true" className="size-4" />,
    key: "delete-workspace",
    label: "Delete workspace",
    destructive: true,
    disabled: true,
    disabledReason: "Owner access required",
  },
];

export function CommandPaletteBasic() {
  return (
    <CommandPalette
      label="Workspace commands"
      description="Search actions, links, and guarded destructive commands."
      commands={commands}
      placeholder="Type a command..."
    />
  );
}

Production-shaped recipes for app shells, dashboards, and AI-native command workflows.

Global launcher

Dialog mode owns focus containment, restoration, close-on-run behavior, and the command surface.

No command run yet.
Show sourceexamples/command-palette/global-launcher.tsx
examples/command-palette/global-launcher.tsx
"use client";

import { useState } from "react";
import {
  CommandPalette,
  CommandPaletteContent,
  CommandPaletteDialog,
  CommandPaletteTrigger,
  type CommandPaletteCommand,
} from "@dethink/components";
import { FolderPlus, PanelTopOpen, Search, Settings2 } from "lucide-react";

export function CommandPaletteGlobalLauncher() {
  const [lastRun, setLastRun] = useState("No command run yet.");

  const commands: CommandPaletteCommand[] = [
    {
      action: () => setLastRun("Created a project draft."),
      group: "Create",
      icon: <FolderPlus aria-hidden="true" className="size-4" />,
      key: "create-project",
      label: "Create project",
      shortcut: "⌘N",
    },
    {
      action: () => setLastRun("Opened the command center."),
      group: "Navigate",
      icon: <PanelTopOpen aria-hidden="true" className="size-4" />,
      key: "command-center",
      label: "Open command center",
      shortcut: "G C",
    },
    {
      action: () => setLastRun("Opened workspace settings."),
      group: "Navigate",
      icon: <Settings2 aria-hidden="true" className="size-4" />,
      key: "settings",
      label: "Open settings",
      shortcut: "G S",
    },
  ];

  return (
    <div className="grid gap-4">
      <CommandPaletteDialog closeOnRun motionPreset="standard">
        <CommandPaletteTrigger variant="outline">
          <Search aria-hidden="true" className="size-4" />
          Open launcher
        </CommandPaletteTrigger>
        <CommandPaletteContent
          title="Command menu"
          description="Run workspace commands from anywhere."
        >
          <CommandPalette
            label="Global command menu"
            commands={commands}
            placeholder="Search commands..."
          />
        </CommandPaletteContent>
      </CommandPaletteDialog>

      <output className="border-border bg-muted/40 text-muted-foreground rounded-md border px-3 py-2 text-sm">
        {lastRun}
      </output>
    </div>
  );
}

A scoped command palette embedded in Sidebar navigation without turning navigation into a form control.

Search remains scoped to the workspace.

Inline command search can live beside persistent navigation without becoming a form control.

Dashboard action runner

Contextual commands run against the active dashboard and report execution through app state.

Run contextual commands against the active dashboard.

Dashboard
Automation
Incident
4 commands available.
Action log
Waiting for an action.
Show sourceexamples/command-palette/dashboard-actions.tsx
examples/command-palette/dashboard-actions.tsx
"use client";

import { useState } from "react";
import {
  CommandPalette,
  type CommandPaletteCommand,
} from "@dethink/components";
import { Bell, Download, RefreshCw, ShieldAlert } from "lucide-react";

export function CommandPaletteDashboardActions() {
  const [status, setStatus] = useState("Waiting for an action.");

  const commands: CommandPaletteCommand[] = [
    {
      action: () => setStatus("Queued a dashboard refresh."),
      description: "Refresh all visible KPI cards",
      group: "Dashboard",
      icon: <RefreshCw aria-hidden="true" className="size-4" />,
      key: "refresh",
      label: "Refresh metrics",
      shortcut: "R",
    },
    {
      action: () => setStatus("Export started."),
      description: "Download the current dashboard as CSV",
      group: "Dashboard",
      icon: <Download aria-hidden="true" className="size-4" />,
      key: "export",
      label: "Export report",
      shortcut: "E",
    },
    {
      action: () => setStatus("Notification rule opened."),
      description: "Create a threshold alert",
      group: "Automation",
      icon: <Bell aria-hidden="true" className="size-4" />,
      key: "alert",
      label: "Create alert",
    },
    {
      destructive: true,
      disabled: true,
      disabledReason: "Requires incident commander role",
      group: "Incident",
      icon: <ShieldAlert aria-hidden="true" className="size-4" />,
      key: "freeze",
      label: "Freeze deploys",
    },
  ];

  return (
    <div className="grid gap-4 lg:grid-cols-[1fr_16rem]">
      <CommandPalette
        label="Dashboard actions"
        description="Run contextual commands against the active dashboard."
        commands={commands}
        onCommandRun={(command) => {
          if (!command.action) {
            setStatus(`Ran ${String(command.textValue ?? command.key)}.`);
          }
        }}
      />
      <div className="border-border bg-muted/40 rounded-md border p-4">
        <div className="text-foreground text-sm font-semibold">Action log</div>
        <output className="text-muted-foreground mt-2 block text-sm leading-6">
          {status}
        </output>
      </div>
    </div>
  );
}

Project and resource switcher

Nested pages, recents, and suggestions create a compact resource browser with Back and Escape handling.

No resource selected.

Recent
Suggested
Browse
Search
5 commands available.
Show sourceexamples/command-palette/project-switcher.tsx
examples/command-palette/project-switcher.tsx
"use client";

import { useState } from "react";
import {
  CommandPalette,
  type CommandPaletteCommand,
} from "@dethink/components";
import { Boxes, FolderGit2, Layers3, Search } from "lucide-react";

const rootCommands: CommandPaletteCommand[] = [
  {
    group: "Browse",
    icon: <FolderGit2 aria-hidden="true" className="size-4" />,
    key: "projects",
    label: "Projects",
    page: "projects",
    shortcut: "G P",
    type: "page",
  },
  {
    group: "Browse",
    icon: <Boxes aria-hidden="true" className="size-4" />,
    key: "resources",
    label: "Resources",
    page: "resources",
    shortcut: "G R",
    type: "page",
  },
  {
    group: "Search",
    icon: <Search aria-hidden="true" className="size-4" />,
    key: "global-search",
    label: "Search all records",
    shortcut: "⌘K",
  },
];

export function CommandPaletteProjectSwitcher() {
  const [lastSelection, setLastSelection] = useState("No resource selected.");

  return (
    <div className="grid gap-4">
      <CommandPalette
        label="Switch workspace context"
        description={lastSelection}
        motionPreset="expressive"
        commands={rootCommands}
        recentCommands={[
          {
            description: "Recently opened project",
            key: "recent-alpha",
            label: "Alpha rollout",
            action: () => setLastSelection("Opened Alpha rollout."),
          },
        ]}
        suggestedCommands={[
          {
            description: "Suggested resource",
            key: "suggested-runbook",
            label: "Payments runbook",
            action: () => setLastSelection("Opened Payments runbook."),
          },
        ]}
        pages={[
          {
            commands: [
              {
                action: () => setLastSelection("Opened Alpha rollout."),
                icon: <Layers3 aria-hidden="true" className="size-4" />,
                key: "alpha",
                label: "Alpha rollout",
              },
              {
                action: () => setLastSelection("Opened Beta migration."),
                icon: <Layers3 aria-hidden="true" className="size-4" />,
                key: "beta",
                label: "Beta migration",
              },
            ],
            description: "Choose an active project.",
            id: "projects",
            title: "Projects",
          },
          {
            commands: [
              {
                action: () => setLastSelection("Opened API gateway."),
                key: "api-gateway",
                label: "API gateway",
              },
              {
                action: () => setLastSelection("Opened billing ledger."),
                key: "billing-ledger",
                label: "Billing ledger",
              },
            ],
            description: "Jump to operational resources.",
            id: "resources",
            title: "Resources",
          },
        ]}
      />
    </div>
  );
}

NavigationMenu keeps persistent links visible while CommandPalette handles keyboard command discovery.

AI command menu

Manual filtering with an app-owned async result window, loading, empty, error, retry, and live status announcements.

Ready for an AI command.

AI actions
2 commands available.

Try runbook, release, or fail.

Show sourceexamples/command-palette/ai-command-menu.tsx
examples/command-palette/ai-command-menu.tsx
"use client";

import { useEffect, useState } from "react";
import {
  CommandPalette,
  type CommandPaletteCommand,
} from "@dethink/components";
import {
  FileText,
  MessageSquareText,
  Sparkles,
  WandSparkles,
} from "lucide-react";

const baseCommands: CommandPaletteCommand[] = [
  {
    description: "Use the current incident notes as context",
    group: "AI actions",
    icon: <Sparkles aria-hidden="true" className="size-4" />,
    key: "summarize-incident",
    label: "Summarize incident",
    shortcut: "S",
  },
  {
    description: "Turn selected bullets into a customer update",
    group: "AI actions",
    icon: <MessageSquareText aria-hidden="true" className="size-4" />,
    key: "draft-update",
    label: "Draft customer update",
  },
];

const remoteCommands: CommandPaletteCommand[] = [
  {
    description: "Find similar incidents and mitigation steps",
    icon: <WandSparkles aria-hidden="true" className="size-4" />,
    key: "retrieve-runbooks",
    keywords: ["runbook", "incident", "knowledge"],
    label: "Retrieve matching runbooks",
  },
  {
    description: "Generate a release note from merged pull requests",
    icon: <FileText aria-hidden="true" className="size-4" />,
    key: "release-note",
    keywords: ["release", "changelog"],
    label: "Draft release note",
  },
  {
    description: "Compare recent evaluations for the selected model",
    icon: <Sparkles aria-hidden="true" className="size-4" />,
    key: "compare-evals",
    keywords: ["eval", "model", "quality"],
    label: "Compare model evaluations",
  },
];

export function CommandPaletteAiCommandMenu() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<CommandPaletteCommand[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [retryCount, setRetryCount] = useState(0);
  const [lastRun, setLastRun] = useState("Ready for an AI command.");

  useEffect(() => {
    const normalizedQuery = query.trim().toLowerCase();

    if (normalizedQuery.length < 2) {
      setResults([]);
      setLoading(false);
      setError(null);
      return undefined;
    }

    setLoading(true);
    setError(null);

    const timeout = window.setTimeout(() => {
      if (normalizedQuery.includes("fail")) {
        setResults([]);
        setError("Could not reach the command index.");
        setLoading(false);
        return;
      }

      setResults(
        remoteCommands
          .filter((command) =>
            [
              command.key,
              typeof command.label === "string" ? command.label : command.key,
              command.description,
              ...(command.keywords ?? []),
            ]
              .filter(Boolean)
              .join(" ")
              .toLowerCase()
              .includes(normalizedQuery),
          )
          .map((command) => ({
            ...command,
            action: () => setLastRun(`Queued ${String(command.label)}.`),
          })),
      );
      setLoading(false);
    }, 300);

    return () => {
      window.clearTimeout(timeout);
    };
  }, [query, retryCount]);

  return (
    <div className="grid gap-4">
      <CommandPalette
        label="AI command menu"
        description={lastRun}
        commands={baseCommands}
        asyncCommands={results}
        query={query}
        onQueryChange={setQuery}
        loading={loading}
        error={error}
        onRetry={() => setRetryCount((count) => count + 1)}
        retryLabel="Retry search"
        minimumQueryLength={2}
        minimumQueryMessage="Type 2 or more characters to search AI commands."
        shouldFilter={false}
        staleMessage="Updating command suggestions."
      />
      <p className="text-muted-foreground text-sm leading-6">
        Try <code className="text-foreground font-mono">runbook</code>,{" "}
        <code className="text-foreground font-mono">release</code>, or{" "}
        <code className="text-foreground font-mono">fail</code>.
      </p>
    </div>
  );
}

Developer notes for the choices that make CommandPalette different from shadcn's command primitive.

Schema and execution

Use CommandPaletteCommand for action, link, page, and separator records. onCommandRun receives source, query, page stack, and close helpers.

Filtering and async

Keep built-in filtering for local commands, or set shouldFilter=false when a server owns the result window. Loading, stale, error, empty, and retry states are first-class props.

Recents and suggestions

recentCommands, suggestedCommands, and asyncCommands merge into predictable source groups with data-source hooks for styling and analytics.

Nested pages

Page commands push typed page definitions onto a stack. Back buttons, Escape handling, page announcements, and controlled pageStack are built in.

Accessibility

Use label or aria-label. Dialog mode restores focus, visible statuses use role=status, and the announcer reports selection, execution, result counts, and page changes.

Theming and motion

All surfaces use provider tokens. motionPreset controls selected-row, result, and page choreography; reducedMotion can force static rendering.

App-shell recipes

Global launchers, Sidebar inline search, NavigationMenu handoff, dashboard action runners, project switchers, and AI menus should stay command workflows rather than form controls.

Migration boundaries

Unlike a shadcn Command copy, this component does not expose cmdk internals or become a Combobox replacement. Keep form selection in Combobox, Select, AsyncSelect, or MultiSelect.

The root data API and the dialog composition API are both exported. Import anatomy primitives when you need custom rows or page frames.

CommandPalette props
PropWhat it doesDefault
commandsCommandPaletteCommand[]Typed action, link, page, and separator items. Commands support labels, descriptions, icons, groups, shortcuts, aliases, keywords, disabled reasons, destructive state, and metadata.[]
query / onQueryChangestring / (query) => voidControls the search text for manual filtering, server search, analytics, or app-owned query persistence.uncontrolled
filter / sort / limit / shouldFilterfunction / function / number / booleanCustomize local ranking, order, result caps, or bypass internal filtering when the app already owns the result window.built-in / none / none / true
recentCommands / suggestedCommands / asyncCommandsCommandPaletteCommand[]Additional source windows merged before base commands with stable data-source attributes and default Recent, Suggested, and Results groups.[]
loading / error / onRetryboolean / ReactNode / () => voidAsync feedback states for remote search, including stale result messaging and accessible retry controls.false / undefined / undefined
pages / pageStack / onPageStackChangeCommandPalettePageDefinition[] / string[] / (stack, context) => voidNested command pages for project switchers, resource browsers, and multistep command flows with Back handling and Escape navigation.[] / uncontrolled / undefined
onCommandRun / closeOnRun(command, context) => void / booleanCentral execution hook with source, query, page, stack, and close helpers. Individual commands can override close behavior.undefined / dialog default
motionPreset / reducedMotion"none" | "subtle" | "standard" | "expressive" / booleanControls result, selected-row, and page-stack choreography while preserving reduced-motion behavior."standard" / prefers-reduced-motion
CommandPaletteDialogDialog compositionWraps Dialog primitives for global launchers, focus containment, restoration, and app-shell command surfaces.closed