Kumo Svelte
  • Home
  • Installation
  • Colors
  • Accessibility
  • Changelog
  • Autocomplete
  • Badge
  • Banner
  • Breadcrumbs
  • Button
  • Checkbox
  • Clipboard Text
  • Cloudflare Logo
  • CodeHighlighted
  • Collapsible
  • Combobox
  • Command Palette
  • Date Picker
  • Dialog
  • Dropdown
  • Empty
  • Flow
  • Grid
  • Input
  • InputArea
  • InputGroup
  • Label
  • Layer Card
  • Link
  • Loader
  • MenuBar
  • Meter
  • Pagination
  • Popover
  • Radio
  • Select
  • Sensitive Input
  • Sidebar
  • Skeleton Line
  • Switch
  • Table
  • Table of Contents
  • Tabs
  • Text
  • Toast
  • Toolbar
  • Tooltip
  • Charts
  • Colors
  • Timeseries
  • Maps
  • Sankey
  • Custom Chart
  • Page Header
  • Resource List
  • Delete Resource
Combobox
kumo-svelte

Combobox

A searchable select component that allows users to filter and select from a list of options.

<script lang="ts">
  import * as Combobox from "kumo-svelte/components/combobox";

  const fruits = ["Apple", "Apricot", "Banana", "Blueberry", "Cherry", "Mango", "Orange", "Pear"];

  let value = $state("Apple");
  let open = $state(false);
</script>

<Combobox.Root bind:value bind:open>
  <Combobox.TriggerInput placeholder="Please select" />
  <Combobox.Content>
    <Combobox.Empty />
    <Combobox.List>
      {#each fruits as fruit}
        <Combobox.Item value={fruit}>{fruit}</Combobox.Item>
      {/each}
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Import

import * as Combobox from "kumo-svelte/components/combobox";

Usage

<script lang="ts">
  import * as Combobox from "kumo-svelte/components/combobox";
  const fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry"];
  let value = $state("Apple");
  let open = $state(false);
</script>

<Combobox.Root bind:value bind:open>
  <Combobox.TriggerInput placeholder="Select a fruit" />
  <Combobox.Content>
    <Combobox.List>
      {#each fruits as fruit}
        <Combobox.Item value={fruit}>{fruit}</Combobox.Item>
      {/each}
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Examples

Sizes

The Combobox supports four size variants that match the Input component: xs, sm, base (default), and lg.

<script lang="ts">
  import * as Combobox from "kumo-svelte/components/combobox";

  const fruits = ["Apple", "Apricot", "Banana", "Blueberry", "Cherry", "Mango", "Orange", "Pear"];

  const sizeFruits = fruits.slice(0, 8);
  let smValue = $state("");
  let baseValue = $state("");
</script>

<div class="flex flex-wrap items-center gap-4">
  <Combobox.Root bind:value={smValue} size="sm">
    <Combobox.TriggerInput placeholder="Small (sm)" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        {#each sizeFruits as fruit}
          <Combobox.Item value={fruit}>{fruit}</Combobox.Item>
        {/each}
      </Combobox.List>
    </Combobox.Content>
  </Combobox.Root>

  <Combobox.Root bind:value={baseValue} size="base">
    <Combobox.TriggerInput placeholder="Base (default)" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        {#each sizeFruits as fruit}
          <Combobox.Item value={fruit}>{fruit}</Combobox.Item>
        {/each}
      </Combobox.List>
    </Combobox.Content>
  </Combobox.Root>
</div>

Size also applies to Combobox.TriggerValue for the searchable-inside variant.

<script lang="ts">
  import * as Combobox from "kumo-svelte/components/combobox";

  const languages = [
    { value: "en", label: "English", emoji: "🇬🇧" },
    { value: "fr", label: "French", emoji: "🇫🇷" },
    { value: "de", label: "German", emoji: "🇩🇪" },
    { value: "es", label: "Spanish", emoji: "🇪🇸" },
    { value: "ja", label: "Japanese", emoji: "🇯🇵" },
  ];

  const languageItems = languages.map((language) => ({
    label: `${language.emoji} ${language.label}`,
    value: language.value,
  }));

  let smValue = $state("en");
  let baseValue = $state("fr");
  let smLabel = $derived(languageItems.find((language) => language.value === smValue)?.label);
  let baseLabel = $derived(languageItems.find((language) => language.value === baseValue)?.label);
</script>

<div class="flex flex-wrap items-center gap-4">
  <Combobox.Root bind:value={smValue} size="sm">
    <Combobox.TriggerValue class="w-[160px]">{smLabel}</Combobox.TriggerValue>
    <Combobox.Content>
      <Combobox.Input placeholder="Search" />
      <Combobox.Empty />
      <Combobox.List>
        {#each languages as language}
          <Combobox.Item value={language.value} label={`${language.emoji} ${language.label}`}>
            {language.emoji} {language.label}
          </Combobox.Item>
        {/each}
      </Combobox.List>
    </Combobox.Content>
  </Combobox.Root>

  <Combobox.Root bind:value={baseValue} size="base">
    <Combobox.TriggerValue class="w-[180px]">{baseLabel}</Combobox.TriggerValue>
    <Combobox.Content>
      <Combobox.Input placeholder="Search" />
      <Combobox.Empty />
      <Combobox.List>
        {#each languages as language}
          <Combobox.Item value={language.value} label={`${language.emoji} ${language.label}`}>
            {language.emoji} {language.label}
          </Combobox.Item>
        {/each}
      </Combobox.List>
    </Combobox.Content>
  </Combobox.Root>
</div>

Searchable Item (Inside)

Use Combobox.TriggerValue for a select-style trigger and put Combobox.Input inside the popup to filter options.

<script lang="ts">
  import * as Combobox from "kumo-svelte/components/combobox";

  const languages = [
    { value: "en", label: "English", emoji: "🇬🇧" },
    { value: "fr", label: "French", emoji: "🇫🇷" },
    { value: "de", label: "German", emoji: "🇩🇪" },
    { value: "es", label: "Spanish", emoji: "🇪🇸" },
    { value: "ja", label: "Japanese", emoji: "🇯🇵" },
  ];

  const languageItems = languages.map((language) => ({
    label: `${language.emoji} ${language.label}`,
    value: language.value,
  }));

  let value = $state("en");
  let open = $state(false);
  let selectedLabel = $derived(languageItems.find((language) => language.value === value)?.label);
</script>

<Combobox.Root bind:value bind:open>
  <Combobox.TriggerValue class="w-[200px]">{selectedLabel}</Combobox.TriggerValue>
  <Combobox.Content>
    <Combobox.Input placeholder="Search languages" />
    <Combobox.Empty />
    <Combobox.List>
      {#each languages as language}
        <Combobox.Item value={language.value} label={`${language.emoji} ${language.label}`}>
          {language.emoji} {language.label}
        </Combobox.Item>
      {/each}
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Searchable Select with Placeholder

Use Combobox.TriggerValue with a placeholder prop to create a searchable Select-style field. The placeholder is displayed until a value is selected.

<script lang="ts">
  import * as Combobox from "kumo-svelte/components/combobox";

  const languages = [
    { value: "en", label: "English", emoji: "🇬🇧" },
    { value: "fr", label: "French", emoji: "🇫🇷" },
    { value: "de", label: "German", emoji: "🇩🇪" },
    { value: "es", label: "Spanish", emoji: "🇪🇸" },
    { value: "ja", label: "Japanese", emoji: "🇯🇵" },
  ];

  const languageItems = languages.map((language) => ({
    label: `${language.emoji} ${language.label}`,
    value: language.value,
  }));

  let value = $state("");
  let open = $state(false);
  let selectedLabel = $derived(languageItems.find((language) => language.value === value)?.label);
</script>

<Combobox.Root bind:value bind:open>
  <Combobox.TriggerValue class="w-[200px]" placeholder="Select a language">
    {selectedLabel}
  </Combobox.TriggerValue>
  <Combobox.Content>
    <Combobox.Input placeholder="Search languages" />
    <Combobox.Empty />
    <Combobox.List>
      {#each languages as language}
        <Combobox.Item value={language.value} label={`${language.emoji} ${language.label}`}>
          {language.emoji} {language.label}
        </Combobox.Item>
      {/each}
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Custom Trigger

Use Combobox.Trigger with a child snippet to replace the default input-like trigger with your own element. Pair it with your selected value label. This is useful for account switchers, sidebar navigation, or anywhere the default chrome does not fit.

<script lang="ts">
  import { Button } from "kumo-svelte/components/button";
  import * as Combobox from "kumo-svelte/components/combobox";
  import CaretUpDownIcon from "phosphor-svelte/lib/CaretUpDownIcon";

  const languages = [
    { value: "en", label: "English", emoji: "🇬🇧" },
    { value: "fr", label: "French", emoji: "🇫🇷" },
    { value: "de", label: "German", emoji: "🇩🇪" },
    { value: "es", label: "Spanish", emoji: "🇪🇸" },
    { value: "ja", label: "Japanese", emoji: "🇯🇵" },
  ];

  const languageItems = languages.map((language) => ({
    label: `${language.emoji} ${language.label}`,
    value: language.value,
  }));

  let value = $state("en");
  let open = $state(false);
  let selectedLabel = $derived(languageItems.find((language) => language.value === value)?.label);
</script>

{#snippet triggerChild({ props }: { props: Record<string, unknown> })}
  <Button {...props} variant="ghost" size="sm">
    <span class="truncate">{selectedLabel}</span>
    <CaretUpDownIcon size={14} class="shrink-0 text-kumo-subtle" />
  </Button>
{/snippet}

<Combobox.Root bind:value bind:open>
  <Combobox.Trigger child={triggerChild} />
  <Combobox.Content>
    <Combobox.Input placeholder="Search languages" />
    <Combobox.Empty />
    <Combobox.List>
      {#each languages as language}
        <Combobox.Item value={language.value} label={`${language.emoji} ${language.label}`}>
          {language.emoji} {language.label}
        </Combobox.Item>
      {/each}
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Grouped

Group items into categories with Combobox.Group and Combobox.GroupLabel.

<script lang="ts">
  import * as Combobox from "kumo-svelte/components/combobox";

  const servers = [
    {
      value: "Asia",
      items: [
        { label: "Japan", value: "japan" },
        { label: "Singapore", value: "singapore" },
        { label: "India", value: "india" },
      ],
    },
    {
      value: "Europe",
      items: [
        { label: "Germany", value: "germany" },
        { label: "France", value: "france" },
        { label: "Netherlands", value: "netherlands" },
      ],
    },
  ];

  let value = $state("");
  let open = $state(false);
</script>

<Combobox.Root bind:value bind:open>
  <Combobox.TriggerInput class="w-[200px]" placeholder="Select server" />
  <Combobox.Content>
    <Combobox.Empty />
    <Combobox.List>
      {#each servers as group}
        <Combobox.Group>
          <Combobox.GroupLabel>{group.value}</Combobox.GroupLabel>
          {#each group.items as item}
            <Combobox.Item value={item.value}>{item.label}</Combobox.Item>
          {/each}
        </Combobox.Group>
      {/each}
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Multiple

Allow users to select multiple options from the list with multiple and a string[] value.

<script lang="ts">
  import { Button } from "kumo-svelte/components/button";
  import { Text } from "kumo-svelte/components/text";
  import * as Combobox from "kumo-svelte/components/combobox";

  const bots = [
    { value: "googlebot", label: "Googlebot", author: "Google" },
    { value: "bingbot", label: "Bingbot", author: "Microsoft" },
    { value: "duckduckbot", label: "DuckDuckBot", author: "DuckDuckGo" },
    { value: "slackbot", label: "Slackbot", author: "Slack" },
    { value: "discordbot", label: "Discordbot", author: "Discord" },
  ];

  let value = $state<string[]>([]);
  let open = $state(false);
  let botLabels = $derived(Object.fromEntries(bots.map((bot) => [bot.value, bot.label])));
</script>

{#snippet selectedBot(value: string)}
  <Combobox.Chip {value}>{botLabels[value] ?? value}</Combobox.Chip>
{/snippet}

<div class="flex gap-2">
  <Combobox.Root multiple bind:value bind:open>
    <Combobox.TriggerMultipleWithInput class="w-[400px]" placeholder="Select bots" renderItem={selectedBot} />
    <Combobox.Content class="max-h-[200px] min-w-auto overflow-y-auto">
      <Combobox.Empty />
      <Combobox.List>
        {#each bots as bot}
          <Combobox.Item value={bot.value} label={bot.label}>
            <div class="flex gap-2">
              <Text>{bot.label}</Text>
              <Text variant="secondary">{bot.author}</Text>
            </div>
          </Combobox.Item>
        {/each}
      </Combobox.List>
    </Combobox.Content>
  </Combobox.Root>
  <Button variant="primary">Submit</Button>
</div>

With Field

Add label, description, and required state using the built-in Field wrapper.

Select your preferred database

<script lang="ts">
  import * as Combobox from "kumo-svelte/components/combobox";

  const databases = [
    { value: "postgres", label: "PostgreSQL" },
    { value: "mysql", label: "MySQL" },
    { value: "mongodb", label: "MongoDB" },
    { value: "redis", label: "Redis" },
    { value: "sqlite", label: "SQLite" },
    { value: "d1", label: "Cloudflare D1" },
  ];

  let value = $state("");
  let open = $state(false);
</script>

<Combobox.Root
  bind:value
  bind:open
  label="Database"
  description="Select your preferred database"
>
  <Combobox.TriggerInput placeholder="Select database" />
  <Combobox.Content>
    <Combobox.Empty />
    <Combobox.List>
      {#each databases as database}
        <Combobox.Item value={database.value}>{database.label}</Combobox.Item>
      {/each}
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Disabled

Pass the disabled prop to prevent interaction.

<script lang="ts">
  import * as Combobox from "kumo-svelte/components/combobox";

  const fruits = ["Apple", "Apricot", "Banana", "Blueberry", "Cherry", "Mango", "Orange", "Pear"];

  const languages = [
    { value: "en", label: "English", emoji: "🇬🇧" },
    { value: "fr", label: "French", emoji: "🇫🇷" },
    { value: "de", label: "German", emoji: "🇩🇪" },
    { value: "es", label: "Spanish", emoji: "🇪🇸" },
    { value: "ja", label: "Japanese", emoji: "🇯🇵" },
  ];

  const languageItems = languages.map((language) => ({
    label: `${language.emoji} ${language.label}`,
    value: language.value,
  }));

  let selectedLanguageLabel = $derived(languageItems.find((language) => language.value === "en")?.label);
</script>

<div class="flex flex-wrap items-start gap-4">
  <Combobox.Root disabled value="Apple">
    <Combobox.TriggerInput class="w-[200px]" placeholder="Select fruit" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        {#each fruits as fruit}
          <Combobox.Item value={fruit}>{fruit}</Combobox.Item>
        {/each}
      </Combobox.List>
    </Combobox.Content>
  </Combobox.Root>

  <Combobox.Root disabled value="en">
    <Combobox.TriggerValue class="w-[200px]">{selectedLanguageLabel}</Combobox.TriggerValue>
    <Combobox.Content>
      <Combobox.Input placeholder="Search" />
      <Combobox.Empty />
      <Combobox.List>
        {#each languages as language}
          <Combobox.Item value={language.value} label={`${language.emoji} ${language.label}`}>
            {language.emoji} {language.label}
          </Combobox.Item>
        {/each}
      </Combobox.List>
    </Combobox.Content>
  </Combobox.Root>
</div>

Disabled Items

Pass the disabled prop to an individual Combobox.Item to make it non-selectable. Disabled rows are rendered with a muted style and skipped during keyboard navigation selection.

<script lang="ts">
  import { Text } from "kumo-svelte/components/text";
  import * as Combobox from "kumo-svelte/components/combobox";

  const disabledDatabases = [
    { value: "postgres", label: "PostgreSQL" },
    { value: "mysql", label: "MySQL" },
    { value: "mariadb", label: "MariaDB", disabled: true, reason: "Beta" },
    { value: "redis", label: "Redis" },
    { value: "d1", label: "Cloudflare D1" },
  ];

  let value = $state("");
  let open = $state(false);
</script>

<div class="w-80">
<Combobox.Root bind:value bind:open>
  <Combobox.TriggerInput placeholder="Select database" />
  <Combobox.Content>
    <Combobox.Empty />
    <Combobox.List>
      {#each disabledDatabases as item}
        <Combobox.Item value={item.value} disabled={item.disabled}>
          {item.label}
          {#if item.reason}
            <Text variant="secondary" size="xs" as="span"> — {item.reason}</Text>
          {/if}
        </Combobox.Item>
      {/each}
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>
</div>

Error State

Display validation errors with the root error prop.

Please select a database

<script lang="ts">
  import * as Combobox from "kumo-svelte/components/combobox";

  const databases = [
    { value: "postgres", label: "PostgreSQL" },
    { value: "mysql", label: "MySQL" },
    { value: "mongodb", label: "MongoDB" },
    { value: "redis", label: "Redis" },
    { value: "sqlite", label: "SQLite" },
    { value: "d1", label: "Cloudflare D1" },
  ];

  let value = $state("");
  let open = $state(false);
</script>

<Combobox.Root
  bind:value
  bind:open
  label="Database"
  error="Please select a database"
  required
>
  <Combobox.TriggerInput placeholder="Select database" />
  <Combobox.Content>
    <Combobox.Empty />
    <Combobox.List>
      {#each databases as database}
        <Combobox.Item value={database.value}>{database.label}</Combobox.Item>
      {/each}
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>

Customizing Dropdown Height

By default, Combobox.Content has a max height of 24rem (384px) or the available viewport space, whichever is smaller. The dropdown scrolls automatically when content exceeds this height.

To customize the max height, pass a class to Combobox.Content:

<!-- Shorter dropdown (200px) -->
<Combobox.Content class="max-h-[200px]">

<!-- Taller dropdown (500px) -->
<Combobox.Content class="max-h-[500px]">

<!-- Use Tailwind presets -->
<Combobox.Content class="max-h-64">
<Combobox.Content class="max-h-96">

API Reference

Combobox

Root component for the searchable select.

PropTypeDefaultDescription
allowDeselectbooleanfalseAllow selecting the current single value again to clear it.
childrenSnippet—Trigger and dropdown content.
defaultValuestring | string[]—Initial selected value. Use a string array when `multiple` is true.
descriptionSnippet | string—Helper text displayed below the combobox.
disabledbooleanfalseDisable the combobox and descendant controls.
errorstring | { message: Snippet | string; match: FieldErrorMatch }—Error message or validation error object.
labelSnippet | string—Label content for the combobox. Enables the built-in Field wrapper.
labelTooltipSnippet—Tooltip content displayed next to the label.
multiplebooleanfalseAllow multiple selections. Use a string array value.
namestring—Form field name.
onOpenChange(open: boolean) => void—Called when the dropdown opens or closes.
onOpenChangeComplete(open: boolean) => void—Called after the open or close transition completes.
onValueChange(value: string | string[]) => void—Called when selection changes.
openbooleanbindableDropdown open state. Bind with `bind:open` for two-way state.
requiredboolean—Mark the field as required.
size"xs" | "sm" | "base" | "lg""base"Size of the combobox trigger. Matches Input component sizes.
valuestring | string[]"" or []Selected value. Bind with `bind:value` for two-way state.

Combobox.Chip

PropTypeDefault
childrenSnippet—
classstring—
removeLabelstring"Remove"
value*string—

Combobox.Content

Dropdown container for the list.

PropTypeDefaultDescription
childrenSnippet—Dropdown content, usually input, empty state, and list.
classstring—Additional CSS classes.
containerPortalProps["to"]—Portal target. Defaults to the configured portal provider container.
sideOffsetnumber4Offset between trigger and content.

Combobox.Empty

PropTypeDefault
childrenSnippet—
classstring—

Combobox.Group

PropTypeDefault
childrenSnippet—

Combobox.GroupLabel

PropTypeDefault
childrenSnippet—
classstring—

Combobox.Input

PropTypeDefault
aria-labelstring—
autocompleteHTMLInputAttributes["autocomplete"]—
classstring—
clearOnDeselectbooleantrue
defaultValuestring—
disabledboolean—
idstring—
namestring—
onblur(event: FocusEvent & { currentTarget: HTMLInputElement }) => void—
oninput(event: Event & { currentTarget: HTMLInputElement }) => void—
onValueChange(value: string) => void—
placeholderstring—
requiredboolean—
sizeKumoComboboxSize—

Combobox.Item

Individual selectable option.

PropTypeDefaultDescription
childrenSnippet—Custom item content. Defaults to `label` or `value`.
classstring—Additional CSS classes.
disabledbooleanfalseDisable this option.
labelstring—Text used for filtering and accessibility. Defaults to `value`.
value*string—Item value.

Combobox.List

PropTypeDefault
childrenSnippet—
classstring—

Combobox.Separator

PropTypeDefault
classstring—

Combobox.Trigger

PropTypeDefault
childSnippet<[{ props: Record<string, unknown> }]>—
childrenSnippet—
classstring—
placeholderstring—

Combobox.TriggerInput

PropTypeDefault
aria-labelstring—
autocompleteHTMLInputAttributes["autocomplete"]—
classstring—
clearLabelstring"Clear selection"
clearOnDeselectbooleantrue
defaultValuestring—
disabledboolean—
onblur(event: FocusEvent & { currentTarget: HTMLInputElement }) => void—
oninput(event: Event & { currentTarget: HTMLInputElement }) => void—
onValueChange(value: string) => void—
placeholderstring—
showOptionsLabelstring"Show options"

Combobox.TriggerMultipleWithInput

PropTypeDefault
childrenSnippet<[value: string]>—
classstring—
clearOnDeselectbooleantrue
disabledboolean—
inputSideKumoComboboxInputSide"right"
onblur(event: FocusEvent & { currentTarget: HTMLInputElement }) => void—
oninput(event: Event & { currentTarget: HTMLInputElement }) => void—
onValueChange(value: string) => void—
placeholderstring—
renderItemSnippet<[value: string]>—

On this page

  • Import
  • Usage
  • Examples
    • Sizes
    • Searchable Item (Inside)
    • Searchable Select with Placeholder
    • Custom Trigger
    • Grouped
    • Multiple
    • With Field
    • Disabled
    • Disabled Items
    • Error State
  • Customizing Dropdown Height
  • API Reference
    • Combobox
    • Combobox.Chip
    • Combobox.Content
    • Combobox.Empty
    • Combobox.Group
    • Combobox.GroupLabel
    • Combobox.Input
    • Combobox.Item
    • Combobox.List
    • Combobox.Separator
    • Combobox.Trigger
    • Combobox.TriggerInput
    • Combobox.TriggerMultipleWithInput