<script lang="ts">
import { Button, CommandPalette } from '$lib';
import { ChartLine, Folder, Gear, House, MagnifyingGlass, Users } from 'phosphor-svelte';
interface CommandItem {
id: string;
title: string;
icon: typeof Folder;
}
interface CommandGroup {
id: string;
label: string;
items: CommandItem[];
}
const sampleGroups: CommandGroup[] = [
{
id: 'commands',
label: 'Commands',
items: [
{ id: 'new-project', title: 'Create New Project', icon: Folder },
{ id: 'settings', title: 'Open Settings', icon: Gear },
{ id: 'search', title: 'Search Files', icon: MagnifyingGlass }
]
},
{
id: 'pages',
label: 'Pages',
items: [
{ id: 'home', title: 'Home', icon: House },
{ id: 'dashboard', title: 'Dashboard', icon: ChartLine },
{ id: 'users', title: 'Users', icon: Users }
]
}
];
let open = $state(false);
let search = $state('');
let selectedItem = $state<string | null>(null);
const filteredGroups = $derived.by(() => {
if (!search) return sampleGroups;
const lowerQuery = search.toLowerCase();
return sampleGroups
.map((group) => ({
...group,
items: group.items.filter((item) => item.title.toLowerCase().includes(lowerQuery))
}))
.filter((group) => group.items.length > 0);
});
function handleSelect(item: CommandItem) {
selectedItem = item.title;
open = false;
search = '';
}
</script>
<div class="flex flex-col items-start gap-4">
<Button onclick={() => (open = true)}>Open Command Palette</Button>
{#if selectedItem}
<p class="text-sm text-kumo-subtle">
Last selected: <span class="text-kumo-default">{selectedItem}</span>
</p>
{/if}
<CommandPalette.Root bind:open shouldFilter={false}>
<CommandPalette.Input bind:value={search} placeholder="Type a command or search..." />
<CommandPalette.List>
{#each filteredGroups as group (group.id)}
<CommandPalette.Group value={group.label}>
<CommandPalette.GroupLabel>{group.label}</CommandPalette.GroupLabel>
<CommandPalette.Items>
{#each group.items as item (item.id)}
{@const Icon = item.icon}
<CommandPalette.Item value={item.title} onSelect={() => handleSelect(item)}>
<span class="flex items-center gap-3">
<span class="text-kumo-subtle">
<Icon size={16} />
</span>
<span>{item.title}</span>
</span>
</CommandPalette.Item>
{/each}
</CommandPalette.Items>
</CommandPalette.Group>
{/each}
<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 'kumo-svelte'; Granular
import { CommandPalette } from 'kumo-svelte/components/command-palette';Usage
CommandPalette is a compound component built on Bits UI's Command primitive. It provides accessible keyboard navigation and customizable styling for command palette interfaces.
<script lang="ts"> import { CommandPalette } from 'kumo-svelte'; interface Item { id: string; title: string; } const items: Item[] = [ { id: '1', title: 'Create Project' }, { id: '2', title: 'Open Settings' } ]; let open = $state(false); let search = $state(''); const filteredItems = $derived( items.filter((item) => item.title.toLowerCase().includes(search.toLowerCase())) );</script><button onclick={() => (open = true)}>Open</button><CommandPalette.Root bind:open shouldFilter={false}> <CommandPalette.Input bind:value={search} placeholder="Search..." /> <CommandPalette.List> {#each filteredItems as item (item.id)} <CommandPalette.Item value={item.title} onSelect={() => (open = false)}> {item.title} </CommandPalette.Item> {/each} <CommandPalette.Empty>No results</CommandPalette.Empty> </CommandPalette.List></CommandPalette.Root>Keyboard Navigation
Built-in keyboard navigation is provided automatically:
- ↑ ↓ Move highlight between items
- Enter Select highlighted item
- ⌘/Ctrl Enter Select with
newTab: true - Escape Close the dialog
Examples
With Grouped Items
Group related commands together with labels.
<script lang="ts">
import { Button, CommandPalette } from '$lib';
import { ChartLine, Folder, Gear, House, MagnifyingGlass, Users } from 'phosphor-svelte';
interface CommandItem {
id: string;
title: string;
icon: typeof Folder;
}
interface CommandGroup {
id: string;
label: string;
items: CommandItem[];
}
const sampleGroups: CommandGroup[] = [
{
id: 'commands',
label: 'Commands',
items: [
{ id: 'new-project', title: 'Create New Project', icon: Folder },
{ id: 'settings', title: 'Open Settings', icon: Gear },
{ id: 'search', title: 'Search Files', icon: MagnifyingGlass }
]
},
{
id: 'pages',
label: 'Pages',
items: [
{ id: 'home', title: 'Home', icon: House },
{ id: 'dashboard', title: 'Dashboard', icon: ChartLine },
{ id: 'users', title: 'Users', icon: Users }
]
}
];
let open = $state(false);
let search = $state('');
let selectedItem = $state<string | null>(null);
const filteredGroups = $derived.by(() => {
if (!search) return sampleGroups;
const lowerQuery = search.toLowerCase();
return sampleGroups
.map((group) => ({
...group,
items: group.items.filter((item) => item.title.toLowerCase().includes(lowerQuery))
}))
.filter((group) => group.items.length > 0);
});
function handleSelect(item: CommandItem) {
selectedItem = item.title;
open = false;
search = '';
}
</script>
<div class="flex flex-col items-start gap-4">
<Button onclick={() => (open = true)}>Open Command Palette</Button>
{#if selectedItem}
<p class="text-sm text-kumo-subtle">
Last selected: <span class="text-kumo-default">{selectedItem}</span>
</p>
{/if}
<CommandPalette.Root bind:open shouldFilter={false}>
<CommandPalette.Input bind:value={search} placeholder="Type a command or search..." />
<CommandPalette.List>
{#each filteredGroups as group (group.id)}
<CommandPalette.Group value={group.label}>
<CommandPalette.GroupLabel>{group.label}</CommandPalette.GroupLabel>
<CommandPalette.Items>
{#each group.items as item (item.id)}
{@const Icon = item.icon}
<CommandPalette.Item value={item.title} onSelect={() => handleSelect(item)}>
<span class="flex items-center gap-3">
<span class="text-kumo-subtle">
<Icon size={16} />
</span>
<span>{item.title}</span>
</span>
</CommandPalette.Item>
{/each}
</CommandPalette.Items>
</CommandPalette.Group>
{/each}
<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.
<script lang="ts">
import { Button, CommandPalette } from '$lib';
const simpleItems = [
{ id: '1', title: 'Copy' },
{ id: '2', title: 'Paste' },
{ id: '3', title: 'Cut' },
{ id: '4', title: 'Delete' },
{ id: '5', title: 'Select All' }
];
let open = $state(false);
let search = $state('');
const filteredItems = $derived(
simpleItems.filter((item) => item.title.toLowerCase().includes(search.toLowerCase()))
);
</script>
<div>
<Button onclick={() => (open = true)}>Open Simple Palette</Button>
<CommandPalette.Root bind:open shouldFilter={false}>
<CommandPalette.Input bind:value={search} placeholder="Search actions..." />
<CommandPalette.List>
{#each filteredItems as item (item.id)}
<CommandPalette.Item value={item.title} onSelect={() => (open = false)}>
{item.title}
</CommandPalette.Item>
{/each}
<CommandPalette.Empty>No actions found</CommandPalette.Empty>
</CommandPalette.List>
</CommandPalette.Root>
</div> Loading State
Show a loading spinner while fetching results.
<script lang="ts">
import { Button, CommandPalette } from '$lib';
import { ChartLine, Folder, Gear, House, MagnifyingGlass, Users } from 'phosphor-svelte';
const sampleGroups = [
{
id: 'commands',
label: 'Commands',
items: [
{ id: 'new-project', title: 'Create New Project', icon: Folder },
{ id: 'settings', title: 'Open Settings', icon: Gear },
{ id: 'search', title: 'Search Files', icon: MagnifyingGlass }
]
},
{
id: 'pages',
label: 'Pages',
items: [
{ id: 'home', title: 'Home', icon: House },
{ id: 'dashboard', title: 'Dashboard', icon: ChartLine },
{ id: 'users', title: 'Users', icon: Users }
]
}
];
let open = $state(false);
let loading = $state(false);
let search = $state('');
let timer: ReturnType<typeof setTimeout> | undefined;
const filteredGroups = $derived.by(() => {
if (!search) return sampleGroups;
const lowerQuery = search.toLowerCase();
return sampleGroups
.map((group) => ({
...group,
items: group.items.filter((item) => item.title.toLowerCase().includes(lowerQuery))
}))
.filter((group) => group.items.length > 0);
});
function handleOpen() {
open = true;
loading = true;
clearTimeout(timer);
timer = setTimeout(() => (loading = false), 1500);
}
</script>
<div>
<Button onclick={handleOpen}>Open with Loading</Button>
<CommandPalette.Root bind:open shouldFilter={false}>
<CommandPalette.Input bind:value={search} placeholder="Search..." />
<CommandPalette.List>
{#if loading}
<CommandPalette.Loading />
{:else}
{#each filteredGroups as group (group.id)}
<CommandPalette.Group value={group.label}>
<CommandPalette.GroupLabel>{group.label}</CommandPalette.GroupLabel>
<CommandPalette.Items>
{#each group.items as item (item.id)}
{@const Icon = item.icon}
<CommandPalette.Item value={item.title} onSelect={() => (open = false)}>
<span class="flex items-center gap-3">
<span class="text-kumo-subtle">
<Icon size={16} />
</span>
<span>{item.title}</span>
</span>
</CommandPalette.Item>
{/each}
</CommandPalette.Items>
</CommandPalette.Group>
{/each}
<CommandPalette.Empty>No results found</CommandPalette.Empty>
{/if}
</CommandPalette.List>
</CommandPalette.Root>
</div> Disabling Browser Autocomplete
Pass input attributes through inputProps to suppress browser and password manager autocomplete overlays.
<script lang="ts">
import { Button, CommandPalette } from '$lib';
import { ChartLine, Folder, Gear, House, MagnifyingGlass, Users } from 'phosphor-svelte';
const sampleGroups = [
{
id: 'commands',
label: 'Commands',
items: [
{ id: 'new-project', title: 'Create New Project', icon: Folder },
{ id: 'settings', title: 'Open Settings', icon: Gear },
{ id: 'search', title: 'Search Files', icon: MagnifyingGlass }
]
},
{
id: 'pages',
label: 'Pages',
items: [
{ id: 'home', title: 'Home', icon: House },
{ id: 'dashboard', title: 'Dashboard', icon: ChartLine },
{ id: 'users', title: 'Users', icon: Users }
]
}
];
let open = $state(false);
let search = $state('');
const filteredGroups = $derived.by(() => {
if (!search) return sampleGroups;
const lowerQuery = search.toLowerCase();
return sampleGroups
.map((group) => ({
...group,
items: group.items.filter((item) => item.title.toLowerCase().includes(lowerQuery))
}))
.filter((group) => group.items.length > 0);
});
</script>
<div class="flex flex-col items-start gap-4">
<Button onclick={() => (open = true)}>Open Palette (No Autocomplete)</Button>
<CommandPalette.Root bind:open shouldFilter={false}>
<CommandPalette.Input
bind:value={search}
placeholder="Search commands..."
autocomplete="off"
autocorrect="off"
autocapitalize="none"
spellcheck={false}
data-1p-ignore="true"
data-lpignore="true"
/>
<CommandPalette.List>
{#each filteredGroups as group (group.id)}
<CommandPalette.Group value={group.label}>
<CommandPalette.GroupLabel>{group.label}</CommandPalette.GroupLabel>
<CommandPalette.Items>
{#each group.items as item (item.id)}
{@const Icon = item.icon}
<CommandPalette.Item value={item.title} onSelect={() => ((open = false), (search = ''))}>
<span class="flex items-center gap-3">
<span class="text-kumo-subtle">
<Icon size={16} />
</span>
<span>{item.title}</span>
</span>
</CommandPalette.Item>
{/each}
</CommandPalette.Items>
</CommandPalette.Group>
{/each}
<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.
<script lang="ts">
import { Button, CommandPalette } from '$lib';
import { File } from 'phosphor-svelte';
const searchResults = [
{ id: '1', title: 'Button', breadcrumbs: ['Components'] },
{ id: '2', title: 'Dialog', breadcrumbs: ['Components'] },
{ id: '3', title: 'Page Header', breadcrumbs: ['Blocks'] }
];
let open = $state(false);
let search = $state('');
const filteredResults = $derived(
searchResults.filter((item) => item.title.toLowerCase().includes(search.toLowerCase()))
);
</script>
<div>
<Button onclick={() => (open = true)}>Open with ResultItem</Button>
<CommandPalette.Root bind:open shouldFilter={false}>
<CommandPalette.Input bind:value={search} placeholder="Search documentation..." />
<CommandPalette.List>
{#each filteredResults as item (item.id)}
<CommandPalette.ResultItem
value={item.title}
title={item.title}
breadcrumbs={item.breadcrumbs}
onSelect={() => (open = false)}
>
{#snippet icon()}
<File size={16} />
{/snippet}
</CommandPalette.ResultItem>
{/each}
<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?: (event: MouseEvent) => void; container?: HTMLElement | string; // Simple shortcut API commands?: CommandPaletteCommand[]; placeholder?: string; value?: string; onValueChange?: (value: string) => void; onSelect?: (command: CommandPaletteCommand, options: { newTab: boolean }) => void; inputProps?: Record<string, unknown>; // Bits UI command options label?: string; loop?: boolean; shouldFilter?: boolean; // Compound API children?: Snippet;} CommandPalette.ResultItem Props
interface CommandPaletteResultItemProps<T> { value: T; title: string; breadcrumbs?: string[]; titleHighlights?: [number, number][]; breadcrumbHighlights?: [number, number][][]; description?: string; icon?: Snippet; onclick?: (event?: MouseEvent) => void; onSelect?: (value: T) => void; showArrow?: boolean; // default: true external?: boolean; // shows external link icon nonInteractive?: boolean;}