CommandPalette
kumo-svelte
<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.

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;}