CommandPalette
@photon-ai/kumo-solid
import { createMemo, createSignal } from "solid-js";
import { CommandPalette, Button } from "@photon-ai/kumo-solid";

export function CommandPaletteBasicDemo() {
  const [open, setOpen] = createSignal(false);
  const [search, setSearch] = createSignal("");
  const [selectedItem, setSelectedItem] = createSignal<string | null>(null);

  const handleSelect = (item: CommandItem) => {
    setSelectedItem(item.title);
    setOpen(false);
    setSearch("");
  };

  // Filter groups based on search
  const filteredGroups = createMemo(() =>
    filterGroupsWithItems(sampleGroups, search()),
  );

  return (
    <div class="flex flex-col items-start gap-4">
      <Button onClick={() => setOpen(true)}>Open Command Palette</Button>
      {selectedItem() && (
        <p class="text-sm text-kumo-subtle">
          Last selected: <span class="text-kumo-default">{selectedItem()}</span>
        </p>
      )}

      <CommandPalette.Root
        open={open()}
        onOpenChange={setOpen}
        items={filteredGroups()}
        value={search()}
        onValueChange={setSearch}
        itemToStringValue={(group) => group.label}
        onSelect={(item, { newTab }) => {
          console.log("Selected:", item.title, newTab ? "(new tab)" : "");
          handleSelect(item);
        }}
        getSelectableItems={getSelectableItems}
      >
        <CommandPalette.Input placeholder="Type a command or search..." />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(group: CommandGroup) => (
              <CommandPalette.Group items={group.items}>
                <CommandPalette.GroupLabel>
                  {group.label}
                </CommandPalette.GroupLabel>
                <CommandPalette.Items>
                  {(item: CommandItem) => (
                    <CommandPalette.Item
                      value={item}
                      onClick={() => handleSelect(item)}
                    >
                      <span class="flex items-center gap-3">
                        {item.icon && (
                          <span class="text-kumo-subtle">{item.icon}</span>
                        )}
                        <span>{item.title}</span>
                      </span>
                    </CommandPalette.Item>
                  )}
                </CommandPalette.Items>
              </CommandPalette.Group>
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>No commands found</CommandPalette.Empty>
        </CommandPalette.List>
        <CommandPalette.Footer>
          <span class="flex items-center gap-2">
            <kbd class="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
              ↑↓
            </kbd>
            <span>Navigate</span>
          </span>
          <span class="flex items-center gap-2">
            <kbd class="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">

            </kbd>
            <span>Select</span>
          </span>
        </CommandPalette.Footer>
      </CommandPalette.Root>
    </div>
  );
}

Installation

Barrel

import { CommandPalette } from "@photon-ai/kumo-solid";

Granular

import { CommandPalette } from "@photon-ai/kumo-solid/components/command-palette";

Usage

CommandPalette is a compound component built on Base UI’s Autocomplete primitive. It provides accessible keyboard navigation and customizable styling for command palette interfaces.

import { createSignal } from "solid-js";
import { CommandPalette } from "@photon-ai/kumo-solid";

interface Item {
  id: string;
  title: string;
}

const items: Item[] = [
  { id: "1", title: "Create Project" },
  { id: "2", title: "Open Settings" },
];

export default function Example() {
  const [open, setOpen] = createSignal(false);
  const [search, setSearch] = createSignal("");

  return (
    <>
      <button onClick={() => setOpen(true)}>Open</button>
      <CommandPalette.Root
        open={open()}
        onOpenChange={setOpen}
        items={items}
        value={search()}
        onValueChange={setSearch}
        itemToStringValue={(item) => item.title}
        getSelectableItems={(items) => [...items]}
      >
        <CommandPalette.Input placeholder="Search..." />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(item) => (
              <CommandPalette.Item
                value={item}
                onClick={() => setOpen(false)}
              >
                {item.title}
              </CommandPalette.Item>
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>No results</CommandPalette.Empty>
        </CommandPalette.List>
      </CommandPalette.Root>
    </>
  );
}

Keyboard Navigation

Built-in keyboard navigation is provided automatically:

+Move highlight between items
EnterSelect highlighted item
⌘/Ctrl+EnterSelect with newTab: true
EscapeClose the dialog

Examples

With Grouped Items

Group related commands together with labels.

import { createMemo, createSignal } from "solid-js";
import { CommandPalette, Button } from "@photon-ai/kumo-solid";

export function CommandPaletteBasicDemo() {
  const [open, setOpen] = createSignal(false);
  const [search, setSearch] = createSignal("");
  const [selectedItem, setSelectedItem] = createSignal<string | null>(null);

  const handleSelect = (item: CommandItem) => {
    setSelectedItem(item.title);
    setOpen(false);
    setSearch("");
  };

  // Filter groups based on search
  const filteredGroups = createMemo(() =>
    filterGroupsWithItems(sampleGroups, search()),
  );

  return (
    <div class="flex flex-col items-start gap-4">
      <Button onClick={() => setOpen(true)}>Open Command Palette</Button>
      {selectedItem() && (
        <p class="text-sm text-kumo-subtle">
          Last selected: <span class="text-kumo-default">{selectedItem()}</span>
        </p>
      )}

      <CommandPalette.Root
        open={open()}
        onOpenChange={setOpen}
        items={filteredGroups()}
        value={search()}
        onValueChange={setSearch}
        itemToStringValue={(group) => group.label}
        onSelect={(item, { newTab }) => {
          console.log("Selected:", item.title, newTab ? "(new tab)" : "");
          handleSelect(item);
        }}
        getSelectableItems={getSelectableItems}
      >
        <CommandPalette.Input placeholder="Type a command or search..." />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(group: CommandGroup) => (
              <CommandPalette.Group items={group.items}>
                <CommandPalette.GroupLabel>
                  {group.label}
                </CommandPalette.GroupLabel>
                <CommandPalette.Items>
                  {(item: CommandItem) => (
                    <CommandPalette.Item
                      value={item}
                      onClick={() => handleSelect(item)}
                    >
                      <span class="flex items-center gap-3">
                        {item.icon && (
                          <span class="text-kumo-subtle">{item.icon}</span>
                        )}
                        <span>{item.title}</span>
                      </span>
                    </CommandPalette.Item>
                  )}
                </CommandPalette.Items>
              </CommandPalette.Group>
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>No commands found</CommandPalette.Empty>
        </CommandPalette.List>
        <CommandPalette.Footer>
          <span class="flex items-center gap-2">
            <kbd class="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
              ↑↓
            </kbd>
            <span>Navigate</span>
          </span>
          <span class="flex items-center gap-2">
            <kbd class="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">

            </kbd>
            <span>Select</span>
          </span>
        </CommandPalette.Footer>
      </CommandPalette.Root>
    </div>
  );
}

Simple Flat List

For simpler use cases, use a flat array of items without grouping.

import { createSignal } from "solid-js";
import { CommandPalette, Button } from "@photon-ai/kumo-solid";

export function CommandPaletteSimpleDemo() {
  const [open, setOpen] = createSignal(false);
  const [search, setSearch] = createSignal("");

  return (
    <div>
      <Button onClick={() => setOpen(true)}>Open Simple Palette</Button>

      <CommandPalette.Root
        open={open()}
        onOpenChange={setOpen}
        items={simpleItems}
        value={search()}
        onValueChange={setSearch}
        itemToStringValue={(item) => item.title}
        onSelect={(item) => {
          console.log("Selected:", item.title);
          setOpen(false);
        }}
        getSelectableItems={(items) => [...items]}
      >
        <CommandPalette.Input placeholder="Search actions..." />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(item: SimpleItem) => (
              <CommandPalette.Item
                value={item}
                onClick={() => {
                  console.log("Clicked:", item.title);
                  setOpen(false);
                }}
              >
                {item.title}
              </CommandPalette.Item>
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>No actions found</CommandPalette.Empty>
        </CommandPalette.List>
      </CommandPalette.Root>
    </div>
  );
}

Loading State

Show a loading spinner while fetching results.

import { createMemo, createSignal } from "solid-js";
import { CommandPalette, Button } from "@photon-ai/kumo-solid";

// With loading state
export function CommandPaletteLoadingDemo() {
  const [open, setOpen] = createSignal(false);
  const [loading, setLoading] = createSignal(false);
  const [search, setSearch] = createSignal("");

  const handleOpen = () => {
    setOpen(true);
    setLoading(true);
    // Simulate loading
    setTimeout(() => setLoading(false), 1500);
  };

  // Filter groups based on search
  const filteredGroups = createMemo(() =>
    filterGroupsWithItems(sampleGroups, search()),
  );

  return (
    <div>
      <Button onClick={handleOpen}>Open with Loading</Button>

      <CommandPalette.Root
        open={open()}
        onOpenChange={setOpen}
        items={loading() ? [] : filteredGroups()}
        value={search()}
        onValueChange={setSearch}
        itemToStringValue={(group) => group.label}
        getSelectableItems={getSelectableItems}
      >
        <CommandPalette.Input placeholder="Search..." />
        <CommandPalette.List>
          {loading() ? (
            <CommandPalette.Loading />
          ) : (
            <>
              <CommandPalette.Results>
                {(group: CommandGroup) => (
                  <CommandPalette.Group items={group.items}>
                    <CommandPalette.GroupLabel>
                      {group.label}
                    </CommandPalette.GroupLabel>
                    <CommandPalette.Items>
                      {(item: CommandItem) => (
                        <CommandPalette.Item
                          value={item}
                          onClick={() => setOpen(false)}
                        >
                          <span class="flex items-center gap-3">
                            {item.icon && (
                              <span class="text-kumo-subtle">{item.icon}</span>
                            )}
                            <span>{item.title}</span>
                          </span>
                        </CommandPalette.Item>
                      )}
                    </CommandPalette.Items>
                  </CommandPalette.Group>
                )}
              </CommandPalette.Results>
              <CommandPalette.Empty>No results found</CommandPalette.Empty>
            </>
          )}
        </CommandPalette.List>
      </CommandPalette.Root>
    </div>
  );
}

Disabling Browser Autocomplete

Pass standard HTML input attributes like autoComplete, autoCorrect, spellCheck, and data-* to suppress browser and password manager autocomplete overlays.

import { createMemo, createSignal } from "solid-js";
import { CommandPalette, Button } from "@photon-ai/kumo-solid";

/** Demonstrates disabling browser autocomplete and spellcheck on the command palette input. */
export function CommandPaletteNoAutocompleteDemo() {
  const [open, setOpen] = createSignal(false);
  const [search, setSearch] = createSignal("");

  const filteredGroups = createMemo(() =>
    filterGroupsWithItems(sampleGroups, search()),
  );

  return (
    <div class="flex flex-col items-start gap-4">
      <Button onClick={() => setOpen(true)}>
        Open Palette (No Autocomplete)
      </Button>

      <CommandPalette.Root
        open={open()}
        onOpenChange={setOpen}
        items={filteredGroups()}
        value={search()}
        onValueChange={setSearch}
        itemToStringValue={(group) => group.label}
        onSelect={(item) => {
          console.log("Selected:", item.title);
          setOpen(false);
          setSearch("");
        }}
        getSelectableItems={getSelectableItems}
      >
        <CommandPalette.Input
          placeholder="Search commands..."
          autoComplete="off"
          autocorrect="off"
          autocapitalize="none"
          spellcheck={false}
          data-1p-ignore="true"
          data-lpignore="true"
        />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(group: CommandGroup) => (
              <CommandPalette.Group items={group.items}>
                <CommandPalette.GroupLabel>
                  {group.label}
                </CommandPalette.GroupLabel>
                <CommandPalette.Items>
                  {(item: CommandItem) => (
                    <CommandPalette.Item
                      value={item}
                      onClick={() => {
                        setOpen(false);
                        setSearch("");
                      }}
                    >
                      <span class="flex items-center gap-3">
                        {item.icon && (
                          <span class="text-kumo-subtle">{item.icon}</span>
                        )}
                        <span>{item.title}</span>
                      </span>
                    </CommandPalette.Item>
                  )}
                </CommandPalette.Items>
              </CommandPalette.Group>
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>No commands found</CommandPalette.Empty>
        </CommandPalette.List>
      </CommandPalette.Root>
    </div>
  );
}

ResultItem with Breadcrumbs

Use ResultItem for rich items with breadcrumbs, icons, and optional text highlighting.

import { createSignal } from "solid-js";
import { CommandPalette, Button } from "@photon-ai/kumo-solid";

export function CommandPaletteResultItemDemo() {
  const [open, setOpen] = createSignal(false);
  const [search, setSearch] = createSignal("");

  return (
    <div>
      <Button onClick={() => setOpen(true)}>Open with ResultItem</Button>

      <CommandPalette.Root
        open={open()}
        onOpenChange={setOpen}
        items={searchResults}
        value={search()}
        onValueChange={setSearch}
        itemToStringValue={(item) => item.title}
        getSelectableItems={(items) => [...items]}
      >
        <CommandPalette.Input placeholder="Search documentation..." />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(item: SearchResult) => (
              <CommandPalette.ResultItem
                value={item}
                title={item.title}
                breadcrumbs={item.breadcrumbs}
                icon={item.icon}
                onClick={() => {
                  console.log("Navigate to:", item.title);
                  setOpen(false);
                }}
              />
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>No pages found</CommandPalette.Empty>
        </CommandPalette.List>
        <CommandPalette.Footer>
          <span class="flex items-center gap-2">
            <kbd class="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
              ↑↓
            </kbd>
            <span>Navigate</span>
          </span>
          <span class="flex items-center gap-2">
            <kbd class="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
              ⌘↵
            </kbd>
            <span>Open in new tab</span>
          </span>
        </CommandPalette.Footer>
      </CommandPalette.Root>
    </div>
  );
}

Component Parts

CommandPalette.Root

The main wrapper that combines Dialog + Panel. Manages open state and Autocomplete functionality.

CommandPalette.Dialog

Modal dialog wrapper. Use with Panel for swappable content (e.g., drill-down navigation).

CommandPalette.Panel

Autocomplete panel without dialog. Use inside Dialog for content that can swap without re-mounting.

CommandPalette.Input

Search input field with auto-focus and keyboard handling.

CommandPalette.List

Scrollable container for results.

CommandPalette.Results

Render prop iterator for items/groups.

CommandPalette.Group

Category grouping container.

CommandPalette.GroupLabel

Section header text within a group.

CommandPalette.Items

Render prop iterator for items within a group.

CommandPalette.Item

Basic selectable item.

CommandPalette.ResultItem

Rich item with breadcrumbs, icons, and text highlighting.

CommandPalette.HighlightedText

Renders text with highlighted portions based on match indices.

CommandPalette.Empty

Empty state when no results match.

CommandPalette.Loading

Loading spinner state.

CommandPalette.Footer

Footer for keyboard hints or other content.

API Reference

CommandPalette.Root Props

interface CommandPaletteRootProps<TGroup, TItem> {
  // Dialog state
  open: boolean;
  onOpenChange: (open: boolean) => void;
  onBackdropClick?: (e: MouseEvent) => void;

  // Autocomplete
  items: readonly TGroup[];
  value?: string;
  onValueChange?: (value: string) => void;
  itemToStringValue?: (item: TGroup) => string;
  filter?: (item: TGroup, query: string) => boolean;
  onItemHighlighted?: (item: TGroup | undefined, details: {...}) => void;

  // Selection
  onSelect?: (item: TItem, options: { newTab: boolean }) => void;
  getSelectableItems?: (items: readonly TGroup[]) => TItem[];

  children: JSX.Element;
}

CommandPalette.ResultItem Props

interface CommandPaletteResultItemProps<T> {
  value: T;
  title: string;
  breadcrumbs?: string[];
  titleHighlights?: [number, number][];
  breadcrumbHighlights?: [number, number][][];
  description?: string;
  icon?: JSX.Element;
  onClick: (e: MouseEvent) => void;
  showArrow?: boolean; // default: true
  external?: boolean; // shows external link icon
  nonInteractive?: boolean;
}