import { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
export function CommandPaletteBasicDemo() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [selectedItem, setSelectedItem] = useState<string | null>(null);
const handleSelect = (item: CommandItem) => {
setSelectedItem(item.title);
setOpen(false);
setSearch("");
};
// Filter groups based on search
const filteredGroups = filterGroupsWithItems(sampleGroups, search);
return (
<div className="flex flex-col items-start gap-4">
<Button onClick={() => setOpen(true)}>Open Command Palette</Button>
{selectedItem && (
<p className="text-sm text-kumo-subtle">
Last selected:{" "}
<span className="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
aria-label="Search commands and pages"
placeholder="Type a command or search..."
/>
<CommandPalette.List>
<CommandPalette.Results>
{(group: CommandGroup) => (
<CommandPalette.Group key={group.id} items={group.items}>
<CommandPalette.GroupLabel>
{group.label}
</CommandPalette.GroupLabel>
<CommandPalette.Items>
{(item: CommandItem) => (
<CommandPalette.Item
key={item.id}
value={item}
onClick={() => handleSelect(item)}
>
<span className="flex items-center gap-3">
{item.icon && (
<span className="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 className="flex items-center gap-2">
<kbd className="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
↑↓
</kbd>
<span>Navigate</span>
</span>
<span className="flex items-center gap-2">
<kbd className="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 "@cloudflare/kumo";Granular
import { CommandPalette } from "@cloudflare/kumo/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.
CommandPalette.Root and CommandPalette.Dialog render a visually hidden
dialog title for screen readers. Set dialogTitle to localized text that
describes the palette’s purpose, such as “Search documentation”.
CommandPalette.Input does not render a visible Label by default.
Placeholder text is only a hint and should not be used as the accessible
name, so provide localized aria-label text when there is no visible label,
or associate custom visible label text with htmlFor/id or
aria-labelledby instead of adding a redundant aria-label.
CommandPalette.Loading and CommandPalette.Empty are announced as polite
status updates. Localize loading text with the label prop, or replace the
loading and empty content with localized children. When your app owns an
async error state, render it with role="alert" so the failure is announced
assertively.
import { useState } from "react";
import { CommandPalette } from "@cloudflare/kumo";
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] = useState(false);
const [search, setSearch] = useState("");
return (
<>
<button type="button" onClick={() => setOpen(true)}>
Open
</button>
<CommandPalette.Root
open={open}
onOpenChange={setOpen}
dialogTitle="Search actions"
items={items}
value={search}
onValueChange={setSearch}
itemToStringValue={(item) => item.title}
getSelectableItems={(items) => items}
>
<CommandPalette.Input
aria-label="Search actions"
placeholder="Search..."
/>
<CommandPalette.List>
<CommandPalette.Results>
{(item) => (
<CommandPalette.Item
key={item.id}
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:
Examples
With Grouped Items
Group related commands together with labels.
import { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
export function CommandPaletteBasicDemo() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [selectedItem, setSelectedItem] = useState<string | null>(null);
const handleSelect = (item: CommandItem) => {
setSelectedItem(item.title);
setOpen(false);
setSearch("");
};
// Filter groups based on search
const filteredGroups = filterGroupsWithItems(sampleGroups, search);
return (
<div className="flex flex-col items-start gap-4">
<Button onClick={() => setOpen(true)}>Open Command Palette</Button>
{selectedItem && (
<p className="text-sm text-kumo-subtle">
Last selected:{" "}
<span className="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
aria-label="Search commands and pages"
placeholder="Type a command or search..."
/>
<CommandPalette.List>
<CommandPalette.Results>
{(group: CommandGroup) => (
<CommandPalette.Group key={group.id} items={group.items}>
<CommandPalette.GroupLabel>
{group.label}
</CommandPalette.GroupLabel>
<CommandPalette.Items>
{(item: CommandItem) => (
<CommandPalette.Item
key={item.id}
value={item}
onClick={() => handleSelect(item)}
>
<span className="flex items-center gap-3">
{item.icon && (
<span className="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 className="flex items-center gap-2">
<kbd className="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
↑↓
</kbd>
<span>Navigate</span>
</span>
<span className="flex items-center gap-2">
<kbd className="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 { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
export function CommandPaletteSimpleDemo() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
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
aria-label="Filter editing actions"
placeholder="Search actions..."
/>
<CommandPalette.List>
<CommandPalette.Results>
{(item: SimpleItem) => (
<CommandPalette.Item
key={item.id}
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 { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
// With loading state
export function CommandPaletteLoadingDemo() {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState("");
const handleOpen = () => {
setOpen(true);
setLoading(true);
// Simulate loading
setTimeout(() => setLoading(false), 1500);
};
// Filter groups based on search
const filteredGroups = 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
aria-label="Search commands while results load"
placeholder="Search..."
/>
<CommandPalette.List>
{loading ? (
<CommandPalette.Loading />
) : (
<>
<CommandPalette.Results>
{(group: CommandGroup) => (
<CommandPalette.Group key={group.id} items={group.items}>
<CommandPalette.GroupLabel>
{group.label}
</CommandPalette.GroupLabel>
<CommandPalette.Items>
{(item: CommandItem) => (
<CommandPalette.Item
key={item.id}
value={item}
onClick={() => setOpen(false)}
>
<span className="flex items-center gap-3">
{item.icon && (
<span className="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 { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
/** Demonstrates disabling browser autocomplete and spellcheck on the command palette input. */
export function CommandPaletteNoAutocompleteDemo() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const filteredGroups = filterGroupsWithItems(sampleGroups, search);
return (
<div className="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
aria-label="Search commands without browser autocomplete"
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 key={group.id} items={group.items}>
<CommandPalette.GroupLabel>
{group.label}
</CommandPalette.GroupLabel>
<CommandPalette.Items>
{(item: CommandItem) => (
<CommandPalette.Item
key={item.id}
value={item}
onClick={() => {
setOpen(false);
setSearch("");
}}
>
<span className="flex items-center gap-3">
{item.icon && (
<span className="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 { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
export function CommandPaletteResultItemDemo() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
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
aria-label="Search documentation pages"
placeholder="Search documentation..."
/>
<CommandPalette.List>
<CommandPalette.Results>
{(item: SearchResult) => (
<CommandPalette.ResultItem
key={item.id}
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 className="flex items-center gap-2">
<kbd className="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
↑↓
</kbd>
<span>Navigate</span>
</span>
<span className="flex items-center gap-2">
<kbd className="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). Pass dialogTitle to provide a localized accessible name for
the dialog.
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. It does not render a
visible Label by default. Provide a localized accessible name with
aria-label when there is no visible label, or associate custom visible label
text with htmlFor/id or aria-labelledby; placeholder text is not a
substitute for a label.
CommandPalette.List
Scrollable container for results. Pass busy while async results are loading
to set aria-busy on the results region.
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. Its children are announced as a polite status update, so pass localized empty-state text.
CommandPalette.Loading
Loading spinner state announced as a polite busy status. Pass a localized
label, or provide localized children when replacing the default spinner.
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;
dialogTitle?: string; // localized accessible dialog title
onBackdropClick?: (e: React.MouseEvent) => void;
// Autocomplete
items: 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: TGroup[]) => TItem[];
children: React.ReactNode;
}CommandPalette.List Props
interface CommandPaletteListProps {
children: React.ReactNode;
busy?: boolean; // sets aria-busy while async results are loading
}CommandPalette.Loading Props
interface CommandPaletteLoadingProps {
children?: React.ReactNode;
label?: React.ReactNode; // localized status text; default: "Loading results"
}CommandPalette.ResultItem Props
interface CommandPaletteResultItemProps<T> {
value: T;
title: string;
breadcrumbs?: string[];
titleHighlights?: [number, number][];
breadcrumbHighlights?: [number, number][][];
description?: string;
icon?: React.ReactNode;
onClick: (e: React.MouseEvent) => void;
showArrow?: boolean; // default: true
external?: boolean; // shows external link icon
nonInteractive?: boolean;
}