# Combobox

Searchable popover-based picker with fuzzy results via fuse.js.

- Package: `@mielui/svelte`
- Component version: `2.4.0`
- Depends on Mielui components: _internal/utils, popover, button, input, scroll-area
- Shared utilities: components/_internal/button-attributes, components/_internal/surface, hugeicons-icon, utils.cn, utils.createContext, utils.inertOutside, utils.travelingHighlight

## Install

~~~~sh
pnpm dlx @mielui/svelte add combobox
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### Content

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> | - | Yes | No | Content rendered inside. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |
| tabindex | number \| undefined | -1 | No | No | Tab index of the panel. |
| role | "dialog" \| "alertdialog" \| "menu" \| "listbox" \| "none" \| undefined | 'none' | No | No | ARIA role of the panel. |
| surface | "solid" \| "glass" \| undefined | - | No | No | Surface treatment. Omit it to inherit `--mielui-surface` from the theme. |
| surfaceClass | string \| undefined | - | No | No | Classes for the inset surface (where children live) — padding, layout, background overrides. The `class` prop styles the outer Panel frame. |
| allowClickOutside | boolean \| undefined | true | No | No | Closes when the pointer is pressed outside it. Set false to keep it open until it is dismissed another way. |
| dismissLayer | boolean \| undefined | true | No | No | Render the full-viewport dismiss layer under the panel while open. Defaults to `true`. Set to `false` for triggers that must stay clickable while open (e.g. an input-style combobox trigger); outside pointer dismissal still applies via `allowClickOutside`. |
| portal | boolean \| undefined | true | No | No | Moves the panel to the end of the document so ancestors cannot clip it. Set false to render it in place. |
| refElement | VirtualElement \| undefined | - | No | No | Positions the panel against this element or virtual element instead of the trigger. The panel then opens to the right of it, as a submenu does. |
| lockScroll | boolean \| undefined | true | No | No | Lock document scrolling while the panel is open. Defaults to `true`. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Item

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| value | string \| (string & string[]) | - | Yes | No | Value this option selects. |
| label | string | - | Yes | No | Text used for search and for the trigger, when it differs from the option's content. |
| callback | (() => void) \| undefined | - | No | No | Called when the item is chosen with the pointer or the keyboard. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| onkeydown | ((event: KeyboardEvent) => void) \| undefined | - | No | No | Called on key down, before the button handles the key itself. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when the button is activated. Loading and disabled buttons do not call it. |
| status | ButtonStatus \| undefined | - | No | No | Controlled visual state. Loading remains focusable and refuses activation. |
| disabled | boolean \| undefined | false | No | No | Prevents activation. A disabled link loses its destination and is skipped by Tab. |
| element | HTMLButtonElement \| HTMLAnchorElement \| undefined | - | No | Yes | Bindable reference to the rendered DOM element. Type is the union of the two possible element types -- narrow at the use site: |
| loading | boolean \| undefined | - | No | No | Convenience alias for `status="loading"`. |
| labels | ButtonLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Label

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> | - | Yes | No | Content rendered inside. |
| class | string \| undefined | - | No | No | Classes added to the element. |



### Results

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| open | boolean \| undefined | false | No | Yes | Whether it is open. Bind it to control the state from outside. |
| onOpenChange | ((open: boolean) => void) \| undefined | - | No | No | Called with the new state whenever it opens or closes. |
| placement | Placement \| undefined | 'bottom' | No | No | Preferred side and alignment relative to the trigger, such as `bottom-start`. It flips when there is no room. |
| hoverable | boolean \| undefined | false | No | No | Opens on hover as well as on click. |
| delay | number \| undefined | 0 | No | No | Milliseconds the pointer rests on the trigger before it opens. |
| closeDelay | number \| undefined | 150 | No | No | Milliseconds after the pointer leaves before it closes. |
| inert | boolean \| undefined | true | No | No | Make document content outside an open non-hover popover inert. Defaults to `true`. |
| labels | ComboboxLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |
| type | "single" \| "multiple" \| undefined | - | No | No | `single` keeps one value. `multiple` allows several and makes `value` an array. |
| value | string \| string[] \| undefined | undefined | No | Yes | Selected value, or an array in multiple mode. Bindable. |
| onValueChange | ((value: string) => void) \| ((value: string[]) => void) \| undefined | - | No | No | Called with the new value when it changes. |



### Trigger

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| style | string \| undefined | - | No | No | Inline styles for the element. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when it is clicked or activated. |
| variant | ButtonVariant \| undefined | 'outline' | No | No | Button style of the trigger. |
| size | "sm" \| "md" \| "lg" \| "icon" \| undefined | 'md' | No | No | Control height of the trigger. |
| unstyled | boolean \| undefined | - | No | No | Forwarded to Button: render with `class` alone, skipping variant/size. |
| icon | boolean \| undefined | - | No | No | Shows the chevron after the label. |
| onopen | (() => void) \| undefined | - | No | No | Called immediately before this trigger opens its popover. |
| aria-labelledby | string \| undefined | - | No | No | Id of the element that names the control, such as a visible label. |
| trailing | Snippet<[]> \| undefined | - | No | No | Content at the end of the trigger. |
| placeholder | string \| undefined | 'Select…' | No | No | Text shown while there is no value. |
| searchPlacement | "trigger" \| "menu" \| undefined | 'trigger' | No | No | `trigger` types into the trigger. `menu` puts a search field inside the menu. |
| threshold | number \| undefined | 0.28 | No | No | How loosely typed text may match, from 0 for exact to 1 for anything. |
| appearance | "button" \| "input" \| undefined | 'button' | No | No | `button` looks like a select. `input` looks like a text field. |
| element | HTMLButtonElement \| HTMLInputElement \| undefined | - | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Public types

~~~~ts
import type {
    PopoverContentProps,
    PopoverProps,
    PopoverTriggerProps
} from '@mielui/svelte/components/popover';
import type { Snippet } from 'svelte';
import type { MenuRowButtonProps } from '../_internal/button-attributes';
import Root from './combobox.svelte';
import Content from './combobox-content.svelte';
import Item from './combobox-item.svelte';
import Label from './combobox-label.svelte';
import Results from './combobox-results.svelte';
import Trigger from './combobox-trigger.svelte';

export type ComboboxItem = {
    id: string;
    value: string;
    label: string;
    callback?: () => void;
    ref: HTMLButtonElement | HTMLAnchorElement | undefined;
};

export type ComboboxState = {
    open: boolean;
    items: Set<ComboboxItem>;
    results: Set<ComboboxItem>;
    searchContent: string;
    searchPlacement: 'trigger' | 'menu';
    threshold: number;
    appearance: 'button' | 'input';
    activeValue?: string;
    selected?: ComboboxItem;
};

export type ComboboxLabels = {
    searchPlaceholder?: string;
    search?: string;
    clear?: string;
    options?: string;
    empty?: string;
};

export type ComboboxRootProps = PopoverProps & {
    /**
     * Overrides the built-in text and accessible names. Every key is optional and English is the
     * fallback.
     */
    labels?: ComboboxLabels;
} & (
        | {
              /** `single` keeps one value. `multiple` allows several and makes `value` an array. */
              type?: 'single';
              /** Selected value, or an array in multiple mode. Bindable. */
              value?: string;
              /** Called with the new value when it changes. */
              onValueChange?: (value: string) => void;
          }
        | {
              /** `single` keeps one value. `multiple` allows several and makes `value` an array. */
              type: 'multiple';
              /** Selected value, or an array in multiple mode. Bindable. */
              value?: string[];
              /** Called with the new value when it changes. */
              onValueChange?: (value: string[]) => void;
          }
    );

export type ComboboxTriggerProps = Omit<
    PopoverTriggerProps,
    'children' | 'element' | 'value' | 'type'
> & {
    /** Id of the element that names the control, such as a visible label. */
    'aria-labelledby'?: string;
    /** Content at the end of the trigger. */
    trailing?: Snippet;
    /** Text shown while there is no value. */
    placeholder?: string;
    /** `trigger` types into the trigger. `menu` puts a search field inside the menu. */
    searchPlacement?: 'trigger' | 'menu';
    /** How loosely typed text may match, from 0 for exact to 1 for anything. */
    threshold?: number;
    /** `button` looks like a select. `input` looks like a text field. */
    appearance?: 'button' | 'input';
    /** Bindable reference to the DOM element. */
    element?: HTMLInputElement | HTMLButtonElement;
};

export type ComboboxContentProps = Omit<PopoverContentProps, 'focusTrap'>;

export type ComboboxItemProps = {
    /** Value this option selects. */
    value: string;
    /** Text used for search and for the trigger, when it differs from the option's content. */
    label: string;
    /** Called when the item is chosen with the pointer or the keyboard. */
    callback?: () => void;
} & MenuRowButtonProps;

export { Content, Item, Label, Results, Root, Trigger };
~~~~

## Examples

### Basic

~~~~svelte
<script lang="ts">
    import * as Combobox from '@mielui/svelte/components/combobox';

    const languages = [
        { value: 'en', label: 'English' },
        { value: 'es', label: 'Spanish' },
        { value: 'fr', label: 'French' },
        { value: 'de', label: 'German' },
        { value: 'ja', label: 'Japanese' },
        { value: 'pt', label: 'Portuguese' }
    ];
</script>

<div class="flex justify-center">
    <div class="w-[240px]">
        <Combobox.Root>
            <Combobox.Trigger placeholder="Select a language" class="w-full" />
            <Combobox.Content>
                <Combobox.Results>
                    {#each languages as item (item.value)}
                        <Combobox.Item value={item.value} label={item.label} />
                    {/each}
                </Combobox.Results>
            </Combobox.Content>
        </Combobox.Root>
    </div>
</div>
~~~~

### Glass

~~~~svelte
<script lang="ts">
    import * as Combobox from '@mielui/svelte/components/combobox';

    const frameworks = [
        { value: 'nextjs', label: 'Next.js' },
        { value: 'sveltekit', label: 'SvelteKit' },
        { value: 'nuxtjs', label: 'Nuxt.js' },
        { value: 'remix', label: 'Remix' },
        { value: 'astro', label: 'Astro' }
    ];
</script>

<div class="flex items-center justify-center">
    <div class="w-[280px]">
        <Combobox.Root>
            <Combobox.Trigger placeholder="Select a framework" class="w-full" />
            <Combobox.Content surface="glass">
                <Combobox.Results>
                    {#each frameworks as item (item.value)}
                        <Combobox.Item value={item.value} label={item.label} />
                    {/each}
                </Combobox.Results>
            </Combobox.Content>
        </Combobox.Root>
    </div>
</div>
~~~~

### Input Search

~~~~svelte
<script lang="ts">
    import * as Combobox from '@mielui/svelte/components/combobox';
    import { Switch } from '@mielui/svelte/components/switch';

    let disabled = $state(false);
    let value = $state('northwind');

    const customers = [
        { value: 'northwind', label: 'Northwind Trading' },
        { value: 'halcyon', label: 'Halcyon Studio' },
        { value: 'kestrel', label: 'Kestrel Logistics' },
        { value: 'mercury', label: 'Mercury Goods' },
        { value: 'assembly', label: 'Assembly Works' }
    ];
</script>

<div class="flex justify-center">
    <div class="flex w-full max-w-[280px] flex-col gap-4">
        <Switch bind:checked={disabled} label="Disable customer selection" />
        <Combobox.Root bind:value>
            <Combobox.Trigger
                {disabled}
                appearance="input"
                placeholder="Search customers"
                class="w-full"
            />
            <Combobox.Content>
                <Combobox.Results>
                    {#each customers as item (item.value)}
                        <Combobox.Item value={item.value} label={item.label} />
                    {/each}
                </Combobox.Results>
            </Combobox.Content>
        </Combobox.Root>
    </div>
</div>
~~~~

### Menu Search

~~~~svelte
<script lang="ts">
    import * as Combobox from '@mielui/svelte/components/combobox';

    const repositories = [
        { value: 'mielui', label: 'mielui/svelte' },
        { value: 'svelte', label: 'sveltejs/svelte' },
        { value: 'vite', label: 'vitejs/vite' },
        { value: 'tailwind', label: 'tailwindlabs/tailwindcss' },
        { value: 'lucide', label: 'lucide-icons/lucide' }
    ];
</script>

<div class="flex justify-center">
    <div class="w-[300px]">
        <Combobox.Root>
            <Combobox.Trigger
                searchPlacement="menu"
                placeholder="Choose a repository"
                class="w-full"
            />
            <Combobox.Content>
                <Combobox.Results>
                    {#each repositories as repository (repository.value)}
                        <Combobox.Item value={repository.value} label={repository.label} />
                    {/each}
                </Combobox.Results>
            </Combobox.Content>
        </Combobox.Root>
    </div>
</div>
~~~~

### Multiple

~~~~svelte
<script lang="ts">
    import * as Combobox from '@mielui/svelte/components/combobox';

    let selected = $state(['design']);
    const teams = [
        { value: 'design', label: 'Design' },
        { value: 'engineering', label: 'Engineering' },
        { value: 'support', label: 'Support' },
        { value: 'operations', label: 'Operations' }
    ];
</script>

<div class="w-full max-w-xs space-y-3">
    <Combobox.Root type="multiple" bind:value={selected}>
        <Combobox.Trigger
            name="teams"
            placeholder="Choose teams"
            searchPlacement="menu"
            class="w-full"
        />
        <Combobox.Content>
            <Combobox.Results>
                {#each teams as team (team.value)}
                    <Combobox.Item value={team.value} label={team.label} />
                {/each}
                <Combobox.Item value="archived" label="Archived team" disabled />
            </Combobox.Results>
        </Combobox.Content>
    </Combobox.Root>
    <p class="text-sm text-foreground-muted" aria-live="polite">
        {selected.length}
        {selected.length === 1 ? 'team' : 'teams'}
        selected
    </p>
</div>
~~~~

### Placement

~~~~svelte
<script lang="ts">
    import * as Combobox from '@mielui/svelte/components/combobox';

    const sizes = ['Small', 'Medium', 'Large', 'Extra large'];
    let loads = $state(0);
</script>

<div class="flex flex-col items-center gap-3">
    <Combobox.Root placement="top-start">
        <Combobox.Trigger
            placeholder="Opens above"
            class="w-56"
            onopen={() => {
                loads += 1;
            }}
        />
        <Combobox.Content>
            <Combobox.Results>
                {#each sizes as size (size)}
                    <Combobox.Item value={size} label={size} />
                {/each}
            </Combobox.Results>
        </Combobox.Content>
    </Combobox.Root>
    <p class="text-sm text-foreground-muted">Options requested {loads} times</p>
</div>
~~~~

### Scrollable

~~~~svelte
<script lang="ts">
    import * as Combobox from '@mielui/svelte/components/combobox';

    const countries = [
        { value: 'ar', label: 'Argentina' },
        { value: 'au', label: 'Australia' },
        { value: 'at', label: 'Austria' },
        { value: 'be', label: 'Belgium' },
        { value: 'br', label: 'Brazil' },
        { value: 'ca', label: 'Canada' },
        { value: 'cl', label: 'Chile' },
        { value: 'cn', label: 'China' },
        { value: 'co', label: 'Colombia' },
        { value: 'cz', label: 'Czechia' },
        { value: 'dk', label: 'Denmark' },
        { value: 'eg', label: 'Egypt' },
        { value: 'fi', label: 'Finland' },
        { value: 'fr', label: 'France' },
        { value: 'de', label: 'Germany' },
        { value: 'gr', label: 'Greece' },
        { value: 'in', label: 'India' },
        { value: 'id', label: 'Indonesia' },
        { value: 'ie', label: 'Ireland' },
        { value: 'il', label: 'Israel' },
        { value: 'it', label: 'Italy' },
        { value: 'jp', label: 'Japan' },
        { value: 'ke', label: 'Kenya' },
        { value: 'kr', label: 'South Korea' },
        { value: 'mx', label: 'Mexico' },
        { value: 'nl', label: 'Netherlands' },
        { value: 'nz', label: 'New Zealand' },
        { value: 'ng', label: 'Nigeria' },
        { value: 'no', label: 'Norway' },
        { value: 'pl', label: 'Poland' },
        { value: 'pt', label: 'Portugal' },
        { value: 'sa', label: 'Saudi Arabia' },
        { value: 'sg', label: 'Singapore' },
        { value: 'za', label: 'South Africa' },
        { value: 'es', label: 'Spain' },
        { value: 'se', label: 'Sweden' },
        { value: 'ch', label: 'Switzerland' },
        { value: 'th', label: 'Thailand' },
        { value: 'tr', label: 'Turkey' },
        { value: 'ae', label: 'United Arab Emirates' },
        { value: 'gb', label: 'United Kingdom' },
        { value: 'us', label: 'United States' }
    ];
</script>

<div class="flex justify-center">
    <div class="w-[260px]">
        <Combobox.Root>
            <Combobox.Trigger placeholder="Select a country" class="w-full" />
            <Combobox.Content class="max-h-56">
                <Combobox.Results>
                    {#each countries as country (country.value)}
                        <Combobox.Item value={country.value} label={country.label} />
                    {/each}
                </Combobox.Results>
            </Combobox.Content>
        </Combobox.Root>
    </div>
</div>
~~~~

### Threshold

~~~~svelte
<script lang="ts">
    import * as Combobox from '@mielui/svelte/components/combobox';

    const airports = [
        { value: 'ams', label: 'Amsterdam Schiphol' },
        { value: 'ath', label: 'Athens Eleftherios Venizelos' },
        { value: 'atl', label: 'Atlanta Hartsfield-Jackson' },
        { value: 'akl', label: 'Auckland' }
    ];
</script>

<div class="grid w-full max-w-lg gap-4 sm:grid-cols-2">
    <div class="grid gap-1.5">
        <span class="text-sm text-foreground-muted">Forgiving, 0.6</span>
        <Combobox.Root>
            <Combobox.Trigger
                appearance="input"
                placeholder="Try typing amstrdm"
                threshold={0.6}
                class="w-full"
            />
            <Combobox.Content>
                <Combobox.Results>
                    {#each airports as airport (airport.value)}
                        <Combobox.Item value={airport.value} label={airport.label} />
                    {/each}
                </Combobox.Results>
            </Combobox.Content>
        </Combobox.Root>
    </div>
    <div class="grid gap-1.5">
        <span class="text-sm text-foreground-muted">Exact, 0</span>
        <Combobox.Root>
            <Combobox.Trigger
                appearance="input"
                placeholder="Try typing amstrdm"
                threshold={0}
                class="w-full"
            />
            <Combobox.Content>
                <Combobox.Results>
                    {#each airports as airport (airport.value)}
                        <Combobox.Item value={airport.value} label={airport.label} />
                    {/each}
                </Combobox.Results>
            </Combobox.Content>
        </Combobox.Root>
    </div>
</div>
~~~~

For the rendered reference, visit [/docs/components/combobox](/docs/components/combobox).
