Combobox
kumo-svelte
<script lang="ts">
  import { Combobox } from 'kumo-svelte/components/combobox';

  const fruits = [
  'Apple',
  'Apricot',
  'Avocado',
  'Banana',
  'Blackberry',
  'Blueberry',
  'Cantaloupe',
  'Cherry',
  'Coconut',
  'Cranberry',
  'Date',
  'Dragon Fruit',
  'Fig',
  'Grape',
  'Grapefruit',
  'Guava',
  'Honeydew',
  'Kiwi',
  'Lemon',
  'Lime',
  'Lychee',
  'Mango',
  'Nectarine',
  'Orange',
  'Papaya',
  'Passion Fruit',
  'Peach',
  'Pear',
  'Pineapple',
  'Plum',
  'Raspberry',
  'Strawberry',
  'Watermelon'
];

  let value = $state<string | null>('Apple');
</script>

<Combobox bind:value items={fruits}>
  <Combobox.TriggerInput placeholder="Please select" />
  <Combobox.Content>
    <Combobox.Empty />
    <Combobox.List>
      {#snippet children(item)}
        <Combobox.Item value={item}>{item}</Combobox.Item>
      {/snippet}
    </Combobox.List>
  </Combobox.Content>
</Combobox>

The Bits UI primitive integration, keyboard-navigation tests, and prop documentation for Combobox were contributed by Teddy (@uhteddy).

Installation

Barrel

import { Combobox } from "kumo-svelte";

Granular

import { Combobox } from "kumo-svelte/components/combobox";

Usage

<script lang="ts">  import { Combobox } from 'kumo-svelte';  const fruits = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];  let value = $state<string | null>(null);</script><Combobox bind:value items={fruits}>  <Combobox.TriggerInput placeholder="Select a fruit" />  <Combobox.Content>    <Combobox.Empty />    <Combobox.List>      {#snippet children(item)}        <Combobox.Item value={item}>{item}</Combobox.Item>      {/snippet}    </Combobox.List>  </Combobox.Content></Combobox>

Examples

Sizes

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

<script lang="ts">
  import { Combobox } from 'kumo-svelte/components/combobox';

  const fruits = [
  'Apple',
  'Apricot',
  'Avocado',
  'Banana',
  'Blackberry',
  'Blueberry',
  'Cantaloupe',
  'Cherry',
  'Coconut',
  'Cranberry',
  'Date',
  'Dragon Fruit',
  'Fig',
  'Grape',
  'Grapefruit',
  'Guava',
  'Honeydew',
  'Kiwi',
  'Lemon',
  'Lime',
  'Lychee',
  'Mango',
  'Nectarine',
  'Orange',
  'Papaya',
  'Passion Fruit',
  'Peach',
  'Pear',
  'Pineapple',
  'Plum',
  'Raspberry',
  'Strawberry',
  'Watermelon'
];
</script>

<div class="flex flex-wrap items-center gap-4">
  <Combobox size="sm" items={fruits.slice(0, 8)}>
    <Combobox.TriggerInput placeholder="Small (sm)" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        {#snippet children(item)}
          <Combobox.Item value={item}>{item}</Combobox.Item>
        {/snippet}
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
  <Combobox size="base" items={fruits.slice(0, 8)}>
    <Combobox.TriggerInput placeholder="Base (default)" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        {#snippet children(item)}
          <Combobox.Item value={item}>{item}</Combobox.Item>
        {/snippet}
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</div>

Size also applies to TriggerValue (searchable inside variant):

<script lang="ts">
  import { Combobox } from 'kumo-svelte/components/combobox';

  const languages = [
    { value: 'en', label: 'English', emoji: 'GB' },
    { value: 'fr', label: 'French', emoji: 'FR' }
  ];
</script>

<div class="flex flex-wrap items-center gap-4">
  <Combobox size="sm" value={languages[0]} items={languages}>
    <Combobox.TriggerValue class="w-[160px]" />
    <Combobox.Content>
      <Combobox.Input placeholder="Search" />
      <Combobox.Empty />
      <Combobox.List>
        {#snippet children(item)}
          <Combobox.Item value={item}>{item.emoji} {item.label}</Combobox.Item>
        {/snippet}
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
  <Combobox size="base" value={languages[1]} items={languages}>
    <Combobox.TriggerValue class="w-[180px]" />
    <Combobox.Content>
      <Combobox.Input placeholder="Search" />
      <Combobox.Empty />
      <Combobox.List>
        {#snippet children(item)}
          <Combobox.Item value={item}>{item.emoji} {item.label}</Combobox.Item>
        {/snippet}
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</div>

Searchable Item (Inside)

A searchable select component inside popup that allows users to filter and select.

<script lang="ts">
  import { Combobox } from 'kumo-svelte/components/combobox';

  const languages = [
    { value: 'en', label: 'English', emoji: 'GB' },
    { value: 'fr', label: 'French', emoji: 'FR' },
    { value: 'de', label: 'German', emoji: 'DE' },
    { value: 'es', label: 'Spanish', emoji: 'ES' }
  ];

  let value = $state<any>(languages[0]);
</script>

<Combobox bind:value items={languages}>
  <Combobox.TriggerValue class="w-[200px]" />
  <Combobox.Content>
    <Combobox.Input placeholder="Search languages" />
    <Combobox.Empty />
    <Combobox.List>
      {#snippet children(item)}
        <Combobox.Item value={item}>{item.emoji} {item.label}</Combobox.Item>
      {/snippet}
    </Combobox.List>
  </Combobox.Content>
</Combobox>

Searchable Select with Placeholder

Use 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 { Combobox } from 'kumo-svelte/components/combobox';

  const languages = [
    { value: 'en', label: 'English', emoji: 'GB' },
    { value: 'fr', label: 'French', emoji: 'FR' },
    { value: 'de', label: 'German', emoji: 'DE' },
    { value: 'es', label: 'Spanish', emoji: 'ES' }
  ];

  let value = $state<any>(null);
</script>

<Combobox bind:value items={languages}>
  <Combobox.TriggerValue class="w-[200px]" placeholder="Select a language" />
  <Combobox.Content>
    <Combobox.Input placeholder="Search languages" />
    <Combobox.Empty />
    <Combobox.List>
      {#snippet children(item)}
        <Combobox.Item value={item}>{item.emoji} {item.label}</Combobox.Item>
      {/snippet}
    </Combobox.List>
  </Combobox.Content>
</Combobox>

Object Item Collections

Derive stable values and labels from application objects with Combobox.createItems().

<script lang="ts">
  import { Combobox } from 'kumo-svelte/components/combobox';

  type Fruit = { id: string; label: string };
  const fruits = Combobox.createItems<Fruit>(
    [
      { id: 'apple', label: 'Apple' },
      { id: 'banana', label: 'Banana' }
    ],
    { getValue: (fruit) => fruit.id, getLabel: (fruit) => fruit.label }
  );
  let value = $state<Fruit | null>(null);
</script>

<Combobox items={fruits} bind:value>
  <Combobox.TriggerInput placeholder="Select a fruit" />
  <Combobox.Content>
    <Combobox.List>
      {#snippet children(fruit)}
        <Combobox.Item value={fruit}>{fruit.label}</Combobox.Item>
      {/snippet}
    </Combobox.List>
  </Combobox.Content>
</Combobox>

Custom Trigger

Use Combobox.Trigger with a render prop to replace the default input-like trigger with your own element. Pair with Combobox.Value to display the selected value. Useful for account switchers, sidebar navigation, or anywhere the default chrome doesn't fit.

<script lang="ts">
  import { Combobox } from 'kumo-svelte/components/combobox';
  import MagnifyingGlass from 'phosphor-svelte/lib/MagnifyingGlass';

  const languages = [
    { value: 'en', label: 'English', emoji: 'GB' },
    { value: 'fr', label: 'French', emoji: 'FR' }
  ];

  let value = $state(languages[0]);
</script>

<Combobox bind:value items={languages}>
  <Combobox.Trigger class="rounded-md px-2 py-1 text-sm hover:bg-kumo-fill-hover">
    <Combobox.Value>
      <span class="truncate">{value.emoji} {value.label}</span>
    </Combobox.Value>
    <MagnifyingGlass class="size-3.5 shrink-0 text-kumo-subtle" />
  </Combobox.Trigger>
  <Combobox.Content>
    <Combobox.Input placeholder="Search languages" />
    <Combobox.Empty />
    <Combobox.List>
      {#snippet children(item)}
        <Combobox.Item value={item}>{item.emoji} {item.label}</Combobox.Item>
      {/snippet}
    </Combobox.List>
  </Combobox.Content>
</Combobox>

Object Item Collections

Use Combobox.createItems() when items are application objects rather than primitive values. It derives stable selection values and labels while keeping the source object available to the list renderer.

const fruits = Combobox.createItems(  [{ id: 'apple', label: 'Apple' }],  { getValue: (fruit) => fruit.id, getLabel: (fruit) => fruit.label });

Create static collections at module scope. For data that changes at runtime, recreate the collection when the source data changes.

Grouped

Group items into categories using the Group and GroupLabel components.

<script lang="ts">
  import { Combobox } from 'kumo-svelte/components/combobox';

  const servers = [
    { value: 'Asia', items: [{ label: 'Japan', value: 'japan' }, { label: 'Singapore', value: 'singapore' }] },
    { value: 'Europe', items: [{ label: 'Germany', value: 'germany' }, { label: 'France', value: 'france' }] }
  ];
</script>

<Combobox items={servers}>
  <Combobox.TriggerInput class="w-[200px]" placeholder="Select server" />
  <Combobox.Content>
    <Combobox.Empty />
    <Combobox.List>
      {#snippet children(group)}
        <Combobox.Group items={group.items}>
          <Combobox.GroupLabel>{group.value}</Combobox.GroupLabel>
          <Combobox.Collection>
            {#snippet children(item)}
              <Combobox.Item value={item}>{item.label}</Combobox.Item>
            {/snippet}
          </Combobox.Collection>
        </Combobox.Group>
      {/snippet}
    </Combobox.List>
  </Combobox.Content>
</Combobox>

Multiple

Allow users to select multiple options from the list.

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

  const bots = [
    { value: 'googlebot', label: 'Googlebot', author: 'Google' },
    { value: 'bingbot', label: 'Bingbot', author: 'Microsoft' }
  ];

  let value = $state<any[]>([]);
</script>

<div class="flex gap-2">
  <Combobox bind:value items={bots} multiple>
    <Combobox.TriggerMultipleWithInput class="w-[400px]" placeholder="Select bots">
      {#snippet children(selected)}
        <Combobox.Chip value={selected}>{selected.label}</Combobox.Chip>
      {/snippet}
    </Combobox.TriggerMultipleWithInput>
    <Combobox.Content class="max-h-[200px] min-w-auto overflow-y-auto">
      <Combobox.Empty />
      <Combobox.List>
        {#snippet children(item)}
          <Combobox.Item value={item}>
            <div class="flex gap-2">
              <Text>{item.label}</Text>
              <Text variant="secondary">{item.author}</Text>
            </div>
          </Combobox.Item>
        {/snippet}
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
  <Button variant="primary">Submit</Button>
</div>

With Field

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

Select your preferred database
<script lang="ts">
  import { Combobox } from 'kumo-svelte/components/combobox';

  const databases = [
    { value: 'postgres', label: 'PostgreSQL' },
    { value: 'mysql', label: 'MySQL' },
    { value: 'redis', label: 'Redis' }
  ];
</script>

<div class="w-80">
  <Combobox
    items={databases}
    label="Database"
    description="Select your preferred database"
  >
    <Combobox.TriggerInput placeholder="Select database" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        {#snippet children(item)}
          <Combobox.Item value={item}>{item.label}</Combobox.Item>
        {/snippet}
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</div>

Disabled

Pass the disabled prop to prevent interaction. Works with both TriggerInput and TriggerValue.

<script lang="ts">
  import { Combobox } from 'kumo-svelte/components/combobox';

  const fruits = [
  'Apple',
  'Apricot',
  'Avocado',
  'Banana',
  'Blackberry',
  'Blueberry',
  'Cantaloupe',
  'Cherry',
  'Coconut',
  'Cranberry',
  'Date',
  'Dragon Fruit',
  'Fig',
  'Grape',
  'Grapefruit',
  'Guava',
  'Honeydew',
  'Kiwi',
  'Lemon',
  'Lime',
  'Lychee',
  'Mango',
  'Nectarine',
  'Orange',
  'Papaya',
  'Passion Fruit',
  'Peach',
  'Pear',
  'Pineapple',
  'Plum',
  'Raspberry',
  'Strawberry',
  'Watermelon'
];

  const languages = [
    { value: 'en', label: 'English', emoji: 'GB' },
    { value: 'fr', label: 'French', emoji: 'FR' }
  ];
</script>

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

  <Combobox value={languages[0]} items={languages} disabled>
    <Combobox.TriggerValue class="w-[200px]" />
    <Combobox.Content>
      <Combobox.Input placeholder="Search" />
      <Combobox.Empty />
      <Combobox.List>
        {#snippet children(item)}
          <Combobox.Item value={item}>{item.emoji} {item.label}</Combobox.Item>
        {/snippet}
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</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 { Combobox } from 'kumo-svelte/components/combobox';
  import { Text } from 'kumo-svelte/components/text';

  const items = [
    { value: 'postgres', label: 'PostgreSQL' },
    { value: 'mysql', label: 'MySQL' },
    { value: 'mariadb', label: 'MariaDB', disabled: true, reason: 'Beta' },
    { value: 'mongodb', label: 'MongoDB' },
    { value: 'cassandra', label: 'Apache Cassandra', disabled: true, reason: 'Coming soon' }
  ];
</script>

<div class="w-80">
  <Combobox items={items}>
    <Combobox.TriggerInput placeholder="Select database" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        {#snippet children(item)}
          <Combobox.Item value={item} disabled={item.disabled}>
            <span>
              {item.label}
              {#if item.reason}
                <Text variant="secondary" size="xs" as="span"> - {item.reason}</Text>
              {/if}
            </span>
          </Combobox.Item>
        {/snippet}
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</div>

Error State

Display validation errors with the error prop.

Please select a database
<script lang="ts">
  import { Combobox } from 'kumo-svelte/components/combobox';

  const databases = [
    { value: 'postgres', label: 'PostgreSQL' },
    { value: 'mysql', label: 'MySQL' },
    { value: 'redis', label: 'Redis' }
  ];
</script>

<div class="w-80">
  <Combobox
    items={databases}
    label="Database"
    error={{ message: "Please select a database", match: true }}
  >
    <Combobox.TriggerInput placeholder="Select database" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        {#snippet children(item)}
          <Combobox.Item value={item}>{item.label}</Combobox.Item>
        {/snippet}
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</div>

Filtering

Filtering is case- and accent-insensitive by default, powered by Intl.Collator under the hood. For string items, no custom filter is needed.

When filtering on a property of object items, use Combobox.useFilter() to preserve the built-in accent-insensitive matching:

<script lang="ts">  import { Combobox } from "kumo-svelte";  const { contains } = Combobox.useFilter();  const languages = [    { value: "pt", label: "Portuguese", emoji: "🇵🇹" },    { value: "es", label: "Spanish", emoji: "🇪🇸" }  ];  const filter = (item, query) => contains(item.label, query);</script><Combobox items={languages} {filter}>  <!-- ... --></Combobox>

To disable filtering entirely (for example, when results come from a server), pass filter={null}:

<Combobox items={results} filter={null}>  <!-- ... --></Combobox>

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">  // 256px<Combobox.Content class="max-h-96">  // 384px (same as default)

API Reference

Combobox

Root component for the searchable select.

PropTypeDefaultDescription
size 'xs' | 'sm' | 'base' | 'lg'"base"Size preset.
items *ComboboxItem[] | ComboboxItemCollection-Items to display, optionally created with Combobox.createItems().
value unknown-Controlled value.
children Snippet-Child snippet rendered by the component.
class string-Additional classes merged onto the root element.
label string | Snippet-Visible label content.
required boolean-Marks the field as required.
labelTooltip string | Snippet-Optional help content for the label.
description string | Snippet-Supporting description text.
error FieldError-Validation error message or matcher.
onValueChange (value: unknown) => void-Called when the value changes.
multiple booleanfalseEnables multiple selection.
onOpenChange (open: boolean) => void-Called when open state changes.
disabled booleanfalsePrevents interaction with the field.
defaultValue unknown-Initial value.
filter ((item: ComboboxItem, query: string) => boolean) | null-Custom filter function. Set to null to disable filtering.
isItemEqualToValue (item: unknown, value: unknown) => boolean-Custom value equality matcher function.

Combobox.Content

Dropdown container for the list.

PropTypeDefaultDescription
align 'start' | 'center' | 'end'"start"Alignment of the popup relative to the trigger.
alignOffset number | string-Offset along the alignment axis.
side 'top' | 'right' | 'bottom' | 'left'"bottom"Side of the trigger where the popup is placed.
sideOffset number | string4Offset between the popup and the trigger.
customAnchor string | HTMLElement | Measurable | null-Custom element or virtual anchor used to position the popup.
strategy 'absolute' | 'fixed'-CSS positioning strategy used by the floating content.
avoidCollisions booleantrueAdjust placement to avoid crossing collision boundaries.
collisionBoundary Boundary | Boundary[]-Boundary elements used for collision detection.
collisionPadding number | Partial<Record<Side, number>>8Padding kept between the popup and collision boundaries.
sticky 'partial' | 'always'-How the popup remains attached when its anchor approaches a boundary.
hideWhenDetached boolean-Hide the popup when its anchor is fully clipped.
updatePositionStrategy 'optimized' | 'always''optimized'How frequently floating position updates are calculated.
container HTMLElement | string-Portal target for the popup content.

Combobox.Item

Individual selectable option.

PropTypeDefaultDescription
value *ComboboxItem-Controlled value.
disabled boolean-Disables the component.

Additional Sub-components

  • `Combobox.TriggerInput` - Single-select input trigger
  • `Combobox.TriggerValue` - Button trigger showing selected value
  • `Combobox.TriggerMultipleWithInput` - Multi-select with chips
  • `Combobox.Input` - Search input inside dropdown
  • `Combobox.List` - List container with render prop
  • `Combobox.Group` - Group container for categorized items
  • `Combobox.GroupLabel` - Header label for a group
  • `Combobox.Collection` - Items container within a group
  • `Combobox.Chip` - Selected item chip
  • `Combobox.Empty` - Empty state message