# Combobox

**An input with a popup list for searching and selecting values from a collection.**

**Reference:** [View Source](https://github.com/jramke/fluid-primitives/tree/main/Resources/Private/Primitives/Combobox) · [Zag.js Docs](https://zagjs.com/components/react/combobox)

**ComboboxExamples.html**

```html
<ui:listCollection
    items="{
        0: {value: 'amsterdam', label: 'Amsterdam'},
        1: {value: 'athens', label: 'Athens'},
        2: {value: 'barcelona', label: 'Barcelona'},
        3: {value: 'berlin', label: 'Berlin'},
        4: {value: 'copenhagen', label: 'Copenhagen'},
        5: {value: 'dublin', label: 'Dublin'},
        6: {value: 'helsinki', label: 'Helsinki'},
        7: {value: 'lisbon', label: 'Lisbon'},
        8: {value: 'madrid', label: 'Madrid'},
        9: {value: 'oslo', label: 'Oslo'},
        10: {value: 'paris', label: 'Paris'},
        11: {value: 'prague', label: 'Prague'},
        12: {value: 'rome', label: 'Rome'},
        13: {value: 'stockholm', label: 'Stockholm'},
        14: {value: 'vienna', label: 'Vienna'},
        15: {value: 'warsaw', label: 'Warsaw'}
    }"
    as="collection" />

<ui:combobox.root collection="{collection}">
    <ui:combobox.label>Select a city</ui:combobox.label>
    <ui:combobox.control class="min-w-64">
        <ui:combobox.input placeholder="Search for cities" />
        <ui:combobox.clearTrigger />
        <ui:combobox.trigger>
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 opacity-70">
                <path d="m7 15 5 5 5-5"></path>
                <path d="m7 9 5-5 5 5"></path>
            </svg>
        </ui:combobox.trigger>
    </ui:combobox.control>
    <ui:combobox.content>
        <ui:combobox.empty>No cities found.</ui:combobox.empty>
        <ui:combobox.list>
            <f:for each="{collection.items}" as="item">
                <ui:combobox.item item="{item}">
                    <ui:combobox.itemText>{item.label}</ui:combobox.itemText>
                    <ui:combobox.itemIndicator />
                </ui:combobox.item>
            </f:for>
        </ui:combobox.list>
    </ui:combobox.content>
</ui:combobox.root>
```

## Features

- Searchable list with keyboard navigation
- Single and multiple selection
- Disabled items and item groups
- Works with the Field component for forms and validation
- Submits the `value` of the selected items through `hiddenInput`, the visible input shows their labels
- `empty` placeholder for a search without matches
- Locale-aware filtering in the default entry file, replaceable per instance through `onInputValueChange`
- Async, server-rendered search results via `ui:template`
- `dialog` popup type moves the input into the popup, behind a focusable trigger

## Anatomy

`hiddenInput` submits the `value` of the selected items. Place it once anywhere inside `combobox.root` and it renders one visually hidden input per selected value, or a single empty one while nothing is selected. `ui:combobox.root` includes it for you. A [Field](/docs/components/field.md) focuses the combobox through that input when it is the first invalid field of a submitted form, and the input hands the focus on to the visible one.

Place the options inside `list`, which carries the listbox semantics and scrolling. `content` wraps it and is announced as a dialog with the `dialog` popup type. The styled `ui:combobox.content` combines `positioner` and `content` in one part, so you nest `ui:combobox.list` in it yourself.

`combobox.empty` is a placeholder for a collection with nothing to show, because no item matches the search text or an async search is still waiting for results. Place it next to `combobox.list` inside `content`. It shows and hides itself.

```html
<primitives:combobox.root>
    <primitives:combobox.label />
    <primitives:combobox.control>
        <primitives:combobox.input />
        <primitives:combobox.clearTrigger />
        <primitives:combobox.trigger />
    </primitives:combobox.control>
    <primitives:combobox.positioner>
        <primitives:combobox.content>
            <primitives:combobox.empty />
            <primitives:combobox.list>
                <primitives:combobox.item>
                    <primitives:combobox.itemText />
                    <primitives:combobox.itemIndicator />
                </primitives:combobox.item>
                <primitives:combobox.itemGroup>
                    <primitives:combobox.itemGroupLabel />
                </primitives:combobox.itemGroup>
            </primitives:combobox.list>
        </primitives:combobox.content>
    </primitives:combobox.positioner>
    <primitives:combobox.hiddenInput />
</primitives:combobox.root>
```

## Installation

```bash
typo3 ui:add combobox
```

Or copy the files manually from GitHub (https://github.com/jramke/fluid-primitives/tree/main/Resources/Private/Registry/Combobox) into your project.

Read more about installing [Components and Primitives](/docs/core-concepts/primitives.md).

## Examples

### Default value

Pass `defaultValue` to preselect an item. The demo opens with Nigeria in the input, rendered on the server.

**ComboboxExamples.html**

```html
<ui:listCollection
    items="{
        0: {value: 'ng', label: 'Nigeria'},
        1: {value: 'gh', label: 'Ghana'},
        2: {value: 'ke', label: 'Kenya'},
        3: {value: 'ma', label: 'Morocco'},
        4: {value: 'za', label: 'South Africa'},
        5: {value: 'tz', label: 'Tanzania'},
        6: {value: 'ug', label: 'Uganda'},
        7: {value: 'zm', label: 'Zambia'},
        8: {value: 'zw', label: 'Zimbabwe'},
        9: {value: 'sn', label: 'Senegal'},
        10: {value: 'rw', label: 'Rwanda'},
        11: {value: 'ci', label: 'Cote d\'Ivoire'}
    }"
    as="collection" />

<ui:combobox.root collection="{collection}" defaultValue="ng">
    <ui:combobox.label>Country</ui:combobox.label>
    <ui:combobox.control class="min-w-64">
        <ui:combobox.input placeholder="Search African countries" />
        <ui:combobox.clearTrigger />
        <ui:combobox.trigger>
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 opacity-70">
                <path d="m7 15 5 5 5-5"></path>
                <path d="m7 9 5-5 5 5"></path>
            </svg>
        </ui:combobox.trigger>
    </ui:combobox.control>
    <ui:combobox.content>
        <ui:combobox.empty>No countries found.</ui:combobox.empty>
        <ui:combobox.list>
            <f:for each="{collection.items}" as="item">
                <ui:combobox.item item="{item}">
                    <ui:combobox.itemText>{item.label}</ui:combobox.itemText>
                    <ui:combobox.itemIndicator />
                </ui:combobox.item>
            </f:for>
        </ui:combobox.list>
    </ui:combobox.content>
</ui:combobox.root>
```

### Disabled items

Set `disabled: true` on an item of the collection to make it unselectable. Oracle Database and Microsoft SQL Server are disabled in the demo.

**ComboboxExamples.html**

```html
<ui:listCollection
    items="{
        0: {value: 'postgresql', label: 'PostgreSQL'},
        1: {value: 'mysql', label: 'MySQL'},
        2: {value: 'sqlite', label: 'SQLite'},
        3: {value: 'oracle', label: 'Oracle Database', disabled: true},
        4: {value: 'sqlserver', label: 'Microsoft SQL Server', disabled: true},
        5: {value: 'mariadb', label: 'MariaDB'},
        6: {value: 'cockroach', label: 'CockroachDB'},
        7: {value: 'planetscale', label: 'PlanetScale'}
    }"
    as="collection" />

<ui:combobox.root collection="{collection}">
    <ui:combobox.label>Select a database</ui:combobox.label>
    <ui:combobox.control class="min-w-64">
        <ui:combobox.input placeholder="Search databases" />
        <ui:combobox.clearTrigger />
        <ui:combobox.trigger>
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 opacity-70">
                <path d="m7 15 5 5 5-5"></path>
                <path d="m7 9 5-5 5 5"></path>
            </svg>
        </ui:combobox.trigger>
    </ui:combobox.control>
    <ui:combobox.content>
        <ui:combobox.empty>No databases found.</ui:combobox.empty>
        <ui:combobox.list>
            <f:for each="{collection.items}" as="item">
                <ui:combobox.item item="{item}">
                    <ui:combobox.itemText>{item.label}</ui:combobox.itemText>
                    <ui:combobox.itemIndicator />
                </ui:combobox.item>
            </f:for>
        </ui:combobox.list>
    </ui:combobox.content>
</ui:combobox.root>
```

### With item groups

Set `groupByKey` on `ui:listCollection` to group the items, here by their `category`. Type in the demo and groups without a match disappear.

**ComboboxExamples.html**

```html
<ui:listCollection
    items="{
        0: {value: 'typescript', label: 'TypeScript', category: 'Languages'},
        1: {value: 'php', label: 'PHP', category: 'Languages'},
        2: {value: 'rust', label: 'Rust', category: 'Languages'},
        3: {value: 'go', label: 'Go', category: 'Languages'},
        4: {value: 'laravel', label: 'Laravel', category: 'Frameworks'},
        5: {value: 'nextjs', label: 'Next.js', category: 'Frameworks'},
        6: {value: 'symfony', label: 'Symfony', category: 'Frameworks'},
        7: {value: 'astro', label: 'Astro', category: 'Frameworks'},
        8: {value: 'postgresql', label: 'PostgreSQL', category: 'Databases'},
        9: {value: 'sqlite', label: 'SQLite', category: 'Databases'},
        10: {value: 'redis', label: 'Redis', category: 'Databases'},
        11: {value: 'mariadb', label: 'MariaDB', category: 'Databases'}
    }"
    groupByKey="category"
    groupSort="asc"
    as="collection" />

<ui:combobox.root collection="{collection}">
    <ui:combobox.label>Search technologies</ui:combobox.label>
    <ui:combobox.control class="min-w-64">
        <ui:combobox.input placeholder="Search across groups" />
        <ui:combobox.clearTrigger />
        <ui:combobox.trigger>
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 opacity-70">
                <path d="m7 15 5 5 5-5"></path>
                <path d="m7 9 5-5 5 5"></path>
            </svg>
        </ui:combobox.trigger>
    </ui:combobox.control>
    <ui:combobox.content>
        <ui:combobox.empty>No technologies found.</ui:combobox.empty>
        <ui:combobox.list>
            <f:for each="{collection.group}" as="groupItems" key="groupName">
                <ui:combobox.itemGroup value="{groupName}">
                    <ui:combobox.itemGroupLabel class="text-muted-foreground px-2 py-1 text-xs font-medium">{groupName}</ui:combobox.itemGroupLabel>
                    <f:for each="{groupItems}" as="item">
                        <ui:combobox.item item="{item}">
                            <ui:combobox.itemText>{item.label}</ui:combobox.itemText>
                            <ui:combobox.itemIndicator />
                        </ui:combobox.item>
                    </f:for>
                </ui:combobox.itemGroup>
            </f:for>
        </ui:combobox.list>
    </ui:combobox.content>
</ui:combobox.root>
```

### With form field

Wrap the combobox in `ui:field.root` to share label, `name`, required and invalid state. The demo is a required country field.

**ComboboxExamples.html**

```html
<ui:listCollection
    items="{
        0: {value: 'us', label: 'United States'},
        1: {value: 'uk', label: 'United Kingdom'},
        2: {value: 'de', label: 'Germany'},
        3: {value: 'fr', label: 'France'},
        4: {value: 'nl', label: 'Netherlands'},
        5: {value: 'se', label: 'Sweden'},
        6: {value: 'no', label: 'Norway'},
        7: {value: 'dk', label: 'Denmark'},
        8: {value: 'fi', label: 'Finland'},
        9: {value: 'it', label: 'Italy'},
        10: {value: 'es', label: 'Spain'},
        11: {value: 'pt', label: 'Portugal'}
    }"
    as="collection" />

<ui:field.root name="country" required="{true}">
    <ui:combobox.root collection="{collection}">
        <ui:combobox.label>Country</ui:combobox.label>
        <ui:combobox.control class="min-w-64">
            <ui:combobox.input placeholder="Search your country" />
            <ui:combobox.clearTrigger />
            <ui:combobox.trigger>
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 opacity-70">
                    <path d="m7 15 5 5 5-5"></path>
                    <path d="m7 9 5-5 5 5"></path>
                </svg>
            </ui:combobox.trigger>
        </ui:combobox.control>
        <ui:combobox.content>
            <ui:combobox.empty>No countries found.</ui:combobox.empty>
            <ui:combobox.list>
                <f:for each="{collection.items}" as="item">
                    <ui:combobox.item item="{item}">
                        <ui:combobox.itemText>{item.label}</ui:combobox.itemText>
                        <ui:combobox.itemIndicator />
                    </ui:combobox.item>
                </f:for>
            </ui:combobox.list>
        </ui:combobox.content>
    </ui:combobox.root>
    <ui:field.helperText>This description comes from the field primitive.</ui:field.helperText>
    <ui:field.errorText />
</ui:field.root>
```

### Multiple selection

Set `multiple="{true}"` to select several values. The demo lists the selected cities above the input.

**ComboboxExamples.html**

```html
<ui:listCollection
    items="{
        0: {value: 'amsterdam', label: 'Amsterdam'},
        1: {value: 'athens', label: 'Athens'},
        2: {value: 'barcelona', label: 'Barcelona'},
        3: {value: 'berlin', label: 'Berlin'},
        4: {value: 'copenhagen', label: 'Copenhagen'},
        5: {value: 'dublin', label: 'Dublin'},
        6: {value: 'helsinki', label: 'Helsinki'},
        7: {value: 'lisbon', label: 'Lisbon'},
        8: {value: 'madrid', label: 'Madrid'},
        9: {value: 'oslo', label: 'Oslo'},
        10: {value: 'paris', label: 'Paris'},
        11: {value: 'prague', label: 'Prague'},
        12: {value: 'rome', label: 'Rome'},
        13: {value: 'stockholm', label: 'Stockholm'},
        14: {value: 'vienna', label: 'Vienna'},
        15: {value: 'warsaw', label: 'Warsaw'}
    }"
    as="collection" />

<ui:combobox.root collection="{collection}" multiple="{true}" name="destinations[]" autoMount="{false}" rootId="multiple-example">
    <ul {ui:ref(name: 'selectedValuesWrapper' , context: 'combobox' )} role="list" aria-label="Selected values" class="flex items-center gap-2 flex-wrap mb-1">
        <div {ui:ref(name: 'emptyValuesText' , context: 'combobox' )} role="presentation" class="text-sm text-muted-foreground italic">Nothing selected</div>
        <ui:template name="selectedValue" context="combobox">
            <li class="inline-flex items-center justify-center px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 bg-secondary text-secondary-foreground rounded-full"></li>
        </ui:template>
    </ul>

    <ui:combobox.label>Select destinations</ui:combobox.label>
    <ui:combobox.control class="min-w-64">
        <ui:combobox.input placeholder="Search for cities" />
        <ui:combobox.clearTrigger />
        <ui:combobox.trigger>
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 opacity-70">
                <path d="m7 15 5 5 5-5"></path>
                <path d="m7 9 5-5 5 5"></path>
            </svg>
        </ui:combobox.trigger>
    </ui:combobox.control>
    <ui:combobox.content>
        <ui:combobox.empty>No cities found.</ui:combobox.empty>
        <ui:combobox.list>
            <f:for each="{collection.items}" as="item">
                <ui:combobox.item item="{item}">
                    <ui:combobox.itemText>{item.label}</ui:combobox.itemText>
                    <ui:combobox.itemIndicator />
                </ui:combobox.item>
            </f:for>
        </ui:combobox.list>
    </ui:combobox.content>
</ui:combobox.root>

<vite:asset entry="EXT:docs/Resources/Private/Components/ui/ComboboxExamples/Multiple.entry.ts" />
```

### Change the filter

Pass your own `onInputValueChange` in a custom entry file to change how items match, here by the start of the label with `startsWith`. Type "b" in the demo and the list shrinks to the cities starting with B.

**ComboboxExamples.html**

```html
<ui:listCollection
    items="{
        0: {value: 'amsterdam', label: 'Amsterdam'},
        1: {value: 'athens', label: 'Athens'},
        2: {value: 'auckland', label: 'Auckland'},
        3: {value: 'barcelona', label: 'Barcelona'},
        4: {value: 'berlin', label: 'Berlin'},
        5: {value: 'boston', label: 'Boston'},
        6: {value: 'brisbane', label: 'Brisbane'},
        7: {value: 'budapest', label: 'Budapest'},
        8: {value: 'chicago', label: 'Chicago'},
        9: {value: 'copenhagen', label: 'Copenhagen'}
    }"
    as="collection" />

<ui:combobox.root collection="{collection}" autoMount="{false}" rootId="custom-filter-api">
    <ui:combobox.label>Starts-with filter</ui:combobox.label>
    <ui:combobox.control class="min-w-64">
        <ui:combobox.input placeholder="Type something" />
        <ui:combobox.clearTrigger />
        <ui:combobox.trigger>
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 opacity-70">
                <path d="m7 15 5 5 5-5"></path>
                <path d="m7 9 5-5 5 5"></path>
            </svg>
        </ui:combobox.trigger>
    </ui:combobox.control>
    <div class="text-muted-foreground text-xs">Uses a custom mount entry with its own `onInputValueChange` filter (starts-with instead of contains).</div>
    <ui:combobox.content>
        <ui:combobox.empty>No cities found.</ui:combobox.empty>
        <ui:combobox.list>
            <f:for each="{collection.items}" as="item">
                <ui:combobox.item item="{item}">
                    <ui:combobox.itemText>{item.label}</ui:combobox.itemText>
                    <ui:combobox.itemIndicator />
                </ui:combobox.item>
            </f:for>
        </ui:combobox.list>
    </ui:combobox.content>
</ui:combobox.root>

<vite:asset entry="EXT:docs/Resources/Private/Components/ui/ComboboxExamples/CustomFilterApi.entry.ts" />
```

### Async search

Fetch matching items from a server-side search endpoint as people type. Type a city in the demo to see the results come in.

Write the item markup once inside a [`ui:template`](/docs/viewhelpers/template.md) block and pass the endpoint as `searchUrl`. The entry file queries it, clones the template for every result and rebuilds the collection. `combobox.item`, `combobox.itemText` and `combobox.itemIndicator` detect the template on their own, so they take no `value`. `collection` is optional when no items are known up front.

`combobox.empty` shows whenever no item is rendered: before the first query, while a request runs, after an error and for a query without matches. Its content is yours to update, here a spinner and a status text. They are slot content, so their `ui:ref` takes `context: 'combobox'`, as in `{ui:ref(name: 'statusSpinner', context: 'combobox')}`.

The demo sends the query with [`extbase.post()`](/docs/client-utilities/extbase.md), fetches with [`AsyncList`](/docs/client-utilities/async-list.md) and delays the spinner with a [`DelayedIndicator`](/docs/client-utilities/delayed-indicator.md) to avoid flicker on fast responses.

**ComboboxExamples.html**

```html
<ui:combobox.root
    rootId="async-search"
    autoMount="{false}"
    searchUrl="{f:uri.action(action: 'search', controller: 'CitySearch')}">
    <ui:combobox.label>Search for a city</ui:combobox.label>
    <ui:combobox.control class="min-w-64">
        <ui:combobox.input placeholder="Type to search…" />
        <ui:combobox.clearTrigger />
    </ui:combobox.control>
    <div class="text-muted-foreground text-sm">Items are fetched from a search endpoint.</div>
    <ui:combobox.content>
        <ui:combobox.empty>
            <svg {ui:ref(name: 'statusSpinner' , context: 'combobox' )} hidden xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 animate-spin">
                <path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
            </svg>
            <span {ui:ref(name: 'statusText' , context: 'combobox' )}>Start typing to search…</span>
        </ui:combobox.empty>
        <ui:combobox.list>
            <ui:template name="itemTemplate" context="combobox">
                <ui:combobox.item>
                    <ui:combobox.itemText>
                        <span {ui:ref(name: 'title' )}></span>
                        <span class="text-muted-foreground text-xs ms-1" {ui:ref(name: 'description' )}></span>
                    </ui:combobox.itemText>
                    <ui:combobox.itemIndicator />
                </ui:combobox.item>
            </ui:template>
        </ui:combobox.list>
    </ui:combobox.content>
</ui:combobox.root>

<vite:asset entry="EXT:docs/Resources/Private/Components/ui/ComboboxExamples/AsyncSearch.entry.ts" />
```

**AsyncSearch.entry.ts**

```ts
import type { Api as AsyncListApi } from '@zag-js/async-list';
import type { CollectionItem } from '@zag-js/collection';
import { ListCollection } from '@zag-js/collection';
import type { InputValueChangeDetails } from '@zag-js/combobox';
import { debounce } from '@zag-js/utils';
import { AsyncList, DelayedIndicator, extbase, mount, Template } from 'fluid-primitives';
import { Combobox } from 'fluid-primitives/combobox';

interface CityResult extends CollectionItem {
    value: string;
    title: string;
    description?: string;
}

const SEARCH_DEBOUNCE_MS = 300;

type SearchStatus = 'loading' | 'error' | 'empty' | 'idle' | 'results';

const STATUS_TEXT: Record<Exclude<SearchStatus, 'results'>, string> = {
    loading: 'Searching…',
    error: 'Something went wrong. Please try again.',
    empty: 'No results found',
    idle: 'Start typing to search…',
};

// The item list is folded into the same value the status placeholder is driven by, rather than
// updated separately and immediately from the raw subscribe callback - otherwise the old items
// would be removed the instant a new search starts (api.isLoading flips true) while the "Searching…"
// placeholder stays hidden for its own showDelayMs, leaving a visible gap with nothing rendered
// in between. Routing both through one DelayedIndicator means the old items only ever disappear
// at the exact moment something else is ready to take their place - fresh results immediately, or
// the placeholder once it's actually shown.
type SearchState =
    | { status: 'loading' }
    | { status: 'error' }
    | { status: 'empty' }
    | { status: 'idle' }
    | { status: 'results'; items: CityResult[] };

function getSearchState(api: AsyncListApi<CityResult>): SearchState {
    const hasResults = !api.isLoading && !api.error && !api.isEmpty;
    if (hasResults) return { status: 'results', items: api.items };
    if (api.isLoading) return { status: 'loading' };
    if (api.error) return { status: 'error' };
    return api.filter.trim() ? { status: 'empty' } : { status: 'idle' };
}

mount('ui:combobox', 'async-search', ({ props }) => {
    const searchUrl = props.searchUrl as string;
    let insertedItems: HTMLElement[] = [];
    let combobox: Combobox;

    function updateItems(items: CityResult[]) {
        const listEl = combobox.hydrator.query<HTMLElement>('list');
        if (!listEl) return;

        insertedItems.forEach(el => el.remove());
        insertedItems = [];

        const collection = new ListCollection<CityResult>({
            items,
            itemToValue: item => item.value,
            itemToString: item => item.title,
        });

        for (const { value, title, description } of collection) {
            const instance = new Template(combobox.hydrator, 'itemTemplate', { value });

            const titleEl = instance.query<HTMLElement>('title');
            if (titleEl) titleEl.textContent = title;

            const descriptionEl = instance.query<HTMLElement>('description');
            if (description) {
                if (descriptionEl) descriptionEl.textContent = description;
            } else {
                descriptionEl?.remove();
            }

            listEl.appendChild(instance);
            insertedItems.push(instance.root);
        }

        combobox.updateProps({ collection });
    }

    // Renders both the item list and the status placeholder from one incoming state, so the two
    // are always in sync - old items and the old status text/spinner only ever change together,
    // at the moment DelayedIndicator decides something new is actually ready to be shown.
    const searchState = new DelayedIndicator<SearchState>({
        isTransient: s => s.status === 'loading',
        onChange: state => {
            const spinnerEl = combobox.hydrator.query<HTMLElement>('statusSpinner');
            const textEl = combobox.hydrator.query<HTMLElement>('statusText');
            if (!spinnerEl || !textEl) return;

            const hasResults = state.status === 'results';
            updateItems(hasResults ? state.items : []);
            if (hasResults) return;

            spinnerEl.toggleAttribute('hidden', state.status !== 'loading');
            textEl.textContent = STATUS_TEXT[state.status];
        },
    });

    const list = new AsyncList<CityResult>({
        load: async ({ signal, filter }) => {
            if (!filter.trim()) return { items: [] as CityResult[] };

            // post() namespaces { q: filter } under searchUrl's own tx_docs_docs[...]
            // prefix automatically and sends it as a POST body, sidestepping the cHash mismatch
            // a GET query param appended after the fact would otherwise cause (f:uri.action's
            // cHash is computed from the arguments known at build time).
            const response = await extbase.post(searchUrl, { q: filter }, { signal });
            if (!response.ok) {
                throw new Error(`City search failed with status ${response.status}`);
            }
            return { items: (await response.json()) as CityResult[] };
        },
    });

    list.subscribe(api => {
        searchState.set(getSearchState(api));
    });

    // Debounced here rather than inside AsyncList itself - a plain wrap of setFilter, the
    // same way you'd debounce any other callback.
    const setFilterDebounced = debounce(
        (filter: string) => list.setFilter(filter),
        SEARCH_DEBOUNCE_MS
    );

    combobox = new Combobox({
        ...props,
        onInputValueChange: (details: InputValueChangeDetails) => {
            if (details.reason === 'input-change') setFilterDebounced(details.inputValue);
        },
    });

    list.init();
    combobox.init();
    return combobox;
});

```

### Async search with groups

Group the results the endpoint returns, here by country. Write a second `ui:template` for the group (`combobox.itemGroup` with `combobox.itemGroupLabel`), clone it once per group, append the cloned items to the group and the group to `combobox.list`. Pass the group key as the `value` of its [`Template`](/docs/client-utilities/template.md) so the groups stay apart.

**ComboboxExamples.html**

```html
<ui:combobox.root
    rootId="async-search-grouped"
    autoMount="{false}"
    searchUrl="{f:uri.action(action: 'search', controller: 'CitySearch')}">
    <ui:combobox.label>Search for a city</ui:combobox.label>
    <ui:combobox.control class="min-w-64">
        <ui:combobox.input placeholder="Type to search…" />
        <ui:combobox.clearTrigger />
    </ui:combobox.control>
    <div class="text-muted-foreground text-sm">Items are fetched from a search endpoint.</div>
    <ui:combobox.content>
        <ui:combobox.empty class="flex items-center justify-center gap-2">
            <svg {ui:ref(name: 'statusSpinner' , context: 'combobox' )} hidden xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 animate-spin">
                <path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
            </svg>
            <span {ui:ref(name: 'statusText' , context: 'combobox' )}>Start typing to search…</span>
        </ui:combobox.empty>
        <ui:combobox.list>
            <ui:template name="groupTemplate" context="combobox">
                <ui:combobox.itemGroup>
                    <ui:combobox.itemGroupLabel class="text-muted-foreground px-2 py-1 text-xs font-medium">
                        <span {ui:ref(name: 'group-label' )}></span>
                    </ui:combobox.itemGroupLabel>
                </ui:combobox.itemGroup>
            </ui:template>
            <ui:template name="itemTemplate" context="combobox">
                <ui:combobox.item>
                    <ui:combobox.itemText>
                        <span {ui:ref(name: 'title' )}></span>
                    </ui:combobox.itemText>
                    <ui:combobox.itemIndicator />
                </ui:combobox.item>
            </ui:template>
        </ui:combobox.list>
    </ui:combobox.content>
</ui:combobox.root>

<vite:asset entry="EXT:docs/Resources/Private/Components/ui/ComboboxExamples/AsyncSearchGrouped.entry.ts" />
```

**AsyncSearchGrouped.entry.ts**

```ts
import type { Api as AsyncListApi } from '@zag-js/async-list';
import type { CollectionItem } from '@zag-js/collection';
import { ListCollection } from '@zag-js/collection';
import type { InputValueChangeDetails } from '@zag-js/combobox';
import { debounce } from '@zag-js/utils';
import { AsyncList, DelayedIndicator, extbase, mount, Template } from 'fluid-primitives';
import { Combobox } from 'fluid-primitives/combobox';

interface CityResult extends CollectionItem {
    value: string;
    title: string;
    description?: string;
}

const SEARCH_DEBOUNCE_MS = 300;

type SearchStatus = 'loading' | 'error' | 'empty' | 'idle' | 'results';

const STATUS_TEXT: Record<Exclude<SearchStatus, 'results'>, string> = {
    loading: 'Searching…',
    error: 'Something went wrong. Please try again.',
    empty: 'No results found',
    idle: 'Start typing to search…',
};

// The group/item list is folded into the same value the status placeholder is driven by, rather
// than updated separately and immediately from the raw subscribe callback - otherwise the old
// groups would be removed the instant a new search starts (api.isLoading flips true) while the
// "Searching…" placeholder stays hidden for its own showDelayMs, leaving a visible gap with
// nothing rendered in between. Routing both through one DelayedIndicator means the old groups
// only ever disappear at the exact moment something else is ready to take their place - fresh
// results immediately, or the placeholder once it's actually shown.
type SearchState =
    | { status: 'loading' }
    | { status: 'error' }
    | { status: 'empty' }
    | { status: 'idle' }
    | { status: 'results'; items: CityResult[] };

function getSearchState(api: AsyncListApi<CityResult>): SearchState {
    const hasResults = !api.isLoading && !api.error && !api.isEmpty;
    if (hasResults) return { status: 'results', items: api.items };
    if (api.isLoading) return { status: 'loading' };
    if (api.error) return { status: 'error' };
    return api.filter.trim() ? { status: 'empty' } : { status: 'idle' };
}

mount('ui:combobox', 'async-search-grouped', ({ props }) => {
    const searchUrl = props.searchUrl as string;
    let insertedGroups: HTMLElement[] = [];
    let combobox: Combobox;

    function updateItems(items: CityResult[]) {
        const listEl = combobox.hydrator.query<HTMLElement>('list');
        if (!listEl) return;

        insertedGroups.forEach(el => el.remove());
        insertedGroups = [];

        // Grouping by country is baked into the collection itself (groupBy/groupSort), the same
        // way the static "With Item Groups" example groups server-side via groupByKey/groupSort -
        // group() then just reads back what the collection already grouped/sorted.
        const collection = new ListCollection<CityResult>({
            items,
            itemToValue: item => item.value,
            itemToString: item => item.title,
            groupBy: item => item.description || 'Other',
            groupSort: 'asc',
        });

        for (const [country, countryItems] of collection.group()) {
            // The group's own key doubles as its restamp value - stable and unique per group
            // (unlike a random uid()), and what Combobox's own render() needs to correctly link
            // each group's content/label pair via id/aria-labelledby (spreadPropsByValue('itemGroup', ...)).
            const group = new Template(combobox.hydrator, 'groupTemplate', { value: country });

            const labelEl = group.query<HTMLElement>('group-label');
            if (labelEl) labelEl.textContent = country;

            for (const { value, title } of countryItems) {
                const item = new Template(combobox.hydrator, 'itemTemplate', { value });
                const titleEl = item.query<HTMLElement>('title');
                if (titleEl) titleEl.textContent = title;
                group.root.appendChild(item);
            }

            listEl.appendChild(group);
            insertedGroups.push(group.root);
        }

        combobox.updateProps({ collection });
    }

    // Renders both the group/item list and the status placeholder from one incoming state, so
    // the two are always in sync - old groups and the old status text/spinner only ever change
    // together, at the moment DelayedIndicator decides something new is actually ready to be shown.
    const searchState = new DelayedIndicator<SearchState>({
        isTransient: s => s.status === 'loading',
        onChange: state => {
            const spinnerEl = combobox.hydrator.query<HTMLElement>('statusSpinner');
            const textEl = combobox.hydrator.query<HTMLElement>('statusText');
            if (!spinnerEl || !textEl) return;

            const hasResults = state.status === 'results';
            updateItems(hasResults ? state.items : []);
            if (hasResults) return;

            spinnerEl.toggleAttribute('hidden', state.status !== 'loading');
            textEl.textContent = STATUS_TEXT[state.status];
        },
    });

    const list = new AsyncList<CityResult>({
        load: async ({ signal, filter }) => {
            if (!filter.trim()) return { items: [] as CityResult[] };

            // post() namespaces { q: filter } under searchUrl's own tx_docs_docs[...]
            // prefix automatically and sends it as a POST body, sidestepping the cHash mismatch
            // a GET query param appended after the fact would otherwise cause (f:uri.action's
            // cHash is computed from the arguments known at build time).
            const response = await extbase.post(searchUrl, { q: filter }, { signal });
            if (!response.ok) {
                throw new Error(`City search failed with status ${response.status}`);
            }
            return { items: (await response.json()) as CityResult[] };
        },
    });

    list.subscribe(api => {
        searchState.set(getSearchState(api));
    });

    // Debounced here rather than inside AsyncList itself - a plain wrap of setFilter, the
    // same way you'd debounce any other callback.
    const setFilterDebounced = debounce(
        (filter: string) => list.setFilter(filter),
        SEARCH_DEBOUNCE_MS
    );

    combobox = new Combobox({
        ...props,
        onInputValueChange: (details: InputValueChangeDetails) => {
            if (details.reason === 'input-change') setFilterDebounced(details.inputValue);
        },
    });

    list.init();
    combobox.init();
    return combobox;
});

```

### Move the input into the popup

Set `popupType="{f:constant(name: 'Jramke\FluidPrimitives\Enum\PopupType::Dialog')}"` for a popover-style picker: a trigger opens the popup and the `input` sits at the top of the `content`, above the `list`. `content` is announced as a dialog, and the trigger is focusable (its own `focusable` prop overrides that). With the input in the popup, the trigger shows the selection itself, here through a `valueText` element the entry file keeps in sync with `api.valueAsString`. Open the picker in the demo and type to filter.

**ComboboxExamples.html**

```html
<ui:listCollection
    items="{
        0: {value: 'amsterdam', label: 'Amsterdam'},
        1: {value: 'athens', label: 'Athens'},
        2: {value: 'barcelona', label: 'Barcelona'},
        3: {value: 'berlin', label: 'Berlin'},
        4: {value: 'copenhagen', label: 'Copenhagen'},
        5: {value: 'dublin', label: 'Dublin'},
        6: {value: 'helsinki', label: 'Helsinki'},
        7: {value: 'lisbon', label: 'Lisbon'},
        8: {value: 'madrid', label: 'Madrid'},
        9: {value: 'oslo', label: 'Oslo'},
        10: {value: 'paris', label: 'Paris'},
        11: {value: 'prague', label: 'Prague'},
        12: {value: 'rome', label: 'Rome'},
        13: {value: 'stockholm', label: 'Stockholm'},
        14: {value: 'vienna', label: 'Vienna'},
        15: {value: 'warsaw', label: 'Warsaw'}
    }"
    as="collection" />

<ui:combobox.root
    collection="{collection}"
    placeholder="Search for cities"
    popupType="{f:constant(name: 'Jramke\FluidPrimitives\Enum\PopupType::Dialog')}"
    autoMount="{false}"
    rootId="input-in-content">
    <ui:combobox.label>Select a city</ui:combobox.label>
    <ui:combobox.trigger class="!text-foreground !justify-between border-input h-9 w-full min-w-64 gap-2 rounded-md border px-3 text-sm shadow-xs">
        <span {ui:ref(name: 'valueText' , context: 'combobox' )} class="text-muted-foreground truncate">Choose a city</span>
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 opacity-50">
            <path d="m7 15 5 5 5-5"></path>
            <path d="m7 9 5-5 5 5"></path>
        </svg>
    </ui:combobox.trigger>
    <ui:combobox.content>
        <ui:combobox.input class="flex-none border-b" />
        <ui:combobox.empty>No cities found.</ui:combobox.empty>
        <ui:combobox.list>
            <f:for each="{collection.items}" as="item">
                <ui:combobox.item item="{item}">
                    <ui:combobox.itemText>{item.label}</ui:combobox.itemText>
                    <ui:combobox.itemIndicator />
                </ui:combobox.item>
            </f:for>
        </ui:combobox.list>
    </ui:combobox.content>
</ui:combobox.root>

<vite:asset entry="EXT:docs/Resources/Private/Components/ui/ComboboxExamples/InputInContent.entry.ts" />

```

**InputInContent.entry.ts**

```ts
import type { InputValueChangeDetails } from '@zag-js/combobox';
import { createFilter } from '@zag-js/i18n-utils';
import { getGlobal, mount } from 'fluid-primitives';
import { Combobox } from 'fluid-primitives/combobox';

const filter = createFilter({ sensitivity: 'base', locale: getGlobal('locale') });

mount('ui:combobox', 'input-in-content', ({ props }) => {
    const combobox = new Combobox({
        ...props,
        // The trigger shows the selection, so the input starts empty every time the popup opens.
        onOpenChange: ({ open }) => {
            if (!open) combobox.api.setInputValue('');
        },
        onInputValueChange: (details: InputValueChangeDetails) => {
            const source = combobox.getSourceCollection();

            if (details.reason !== 'input-change') {
                combobox.updateProps({ collection: source });
                return;
            }

            const query = details.inputValue.trim();
            combobox.updateProps({
                collection: query
                    ? source.filter(itemString => filter.contains(itemString, query))
                    : source,
            });
        },
    });

    combobox.init();

    // The input lives in the popup, so the trigger has to show the selection itself.
    const valueTextEl = combobox.hydrator.query('valueText');
    if (valueTextEl) {
        const placeholder = valueTextEl.textContent ?? '';
        combobox.machine.subscribe(() => {
            const { valueAsString } = combobox.api;
            valueTextEl.textContent = valueAsString || placeholder;
            valueTextEl.classList.toggle('text-muted-foreground', !valueAsString);
        });
    }

    return combobox;
});

```

### Localization

The labels of the trigger and the clear button ship in the language file and follow the site language. Pass `translations` to override them for one combobox.

```html
<f:variable
    name="comboboxTranslations"
    value="{
        triggerLabel: '{f:translate(key: \'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:combobox.trigger\')}',
        clearTriggerLabel: '{f:translate(key: \'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:combobox.clear\')}'
    }"
/>

<ui:combobox.root translations="{comboboxTranslations}"> ... </ui:combobox.root>
```

## API Reference

### combobox.root

Provides shared combobox state and wraps all related parts. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `collection` | `Domain\Dto\ListCollection` | Yes | `-` | The item collection. |
| `name` | `string` | No | `-` | The `name` attribute of the underlying select. Inherited from a surrounding field when available. |
| `form` | `string` | No | `-` | The associate form of the underlying select. |
| `disabled` | `boolean` | No | `-` | Whether the select is disabled. Inherited from a surrounding field when available. |
| `invalid` | `boolean` | No | `-` | Whether the select is invalid. Inherited from a surrounding field when available. |
| `readOnly` | `boolean` | No | `-` | Whether the select is read-only. Inherited from a surrounding field when available. |
| `required` | `boolean` | No | `-` | Whether the select is required. Inherited from a surrounding field when available. |
| `placeholder` | `string` | No | `-` | The placeholder text of the combobox's input. |
| `defaultOpen` | `boolean` | No | `false` | The initial open state of the combobox when rendered. Use when you don't need to control the open state of the combobox. |
| `defaultValue` | `mixed` | No | `-` | The initial value of the combobox's selected items when rendered. Use when you don't need to control the value of the combobox's selected items. |
| `defaultInputValue` | `string` | No | `-` | The initial value of the combobox's input when rendered. Use when you don't need to control the value of the combobox's input. |
| `defaultHighlightedValue` | `string` | No | `-` | The initial highlighted value of the combobox when rendered. Use when you don't need to control the highlighted value of the combobox. |
| `openOnClick` | `boolean` | No | `false` | Whether to open the combobox popup on initial click on the input. |
| `openOnKeyPress` | `boolean` | No | `true` | Whether to open the combobox on arrow key press. |
| `openOnChange` | `boolean` | No | `true` | Whether to show the combobox when the input value changes. |
| `closeOnSelect` | `boolean` | No | `-` | Whether to close the combobox when an item is selected. Defaults to `true` or if multiple enabled `false`. |
| `loopFocus` | `boolean` | No | `true` | Whether to loop focus within the combobox. |
| `multiple` | `boolean` | No | `-` | Whether to allow multiple selection. **Good to know:** When `multiple` is `true`, the `selectionBehavior` is automatically set to `clear`. It is recommended to render the selected items in a separate container. |
| `allowCustomValue` | `boolean` | No | `false` | Whether to allow typing custom values in the input. |
| `alwaysSubmitOnEnter` | `boolean` | No | `false` | Whether to always submit on Enter key press, even if popup is open. Useful for single-field autocomplete forms where Enter should submit the form. |
| `inputBehavior` | `Enum\ComboboxInputBehavior` | No | `None` | Defines the auto-completion behavior of the combobox. - `autohighlight`: The first focused item is highlighted as the user types - `autocomplete`: Navigating the listbox with the arrow keys selects the item and the input is updated. |
| `selectionBehavior` | `Enum\ComboboxSelectionBehavior` | No | `-` | The behavior of the combobox input when an item is selected - `replace`: The selected item string is set as the input value - `clear`: The input value is cleared - `preserve`: The input value is preserved. Defaults to `replace` or if multiple enabled `clear` |
| `popupType` | `Enum\PopupType` | No | `Listbox` | The kind of popup the combobox's content is: `listbox` for a regular combobox, `dialog` when it holds other interactive content. |
| `autoFocus` | `boolean` | No | `-` | Whether to autofocus the input on mount. |
| `positioning` | `array` | No | `{     "placement": "bottom",     "sameWidth": true }` | The positioning options to dynamically position the menu. |
| `translations` | `array` | No | `-` | Localized combobox labels. Use `f:translate` for per-template localization overrides when needed. |
| `searchUrl` | `string` | No | `-` | Endpoint queried for async search results (e.g. built with f:uri.action in the consuming template). The consumer's own controller/entry.ts owns the request/response shape. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `rootId` | `string` | No | `-` | The root ID of the component, used for hydration and identification. |
| `ids` | `array` | No | `[]` | The IDs of of the component parts for composition. |
| `autoMount` | `boolean` | No | `true` | Whether the client initializes the component on its own. Set it to false to mount it yourself with `mount()` |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-root` | <uid> |
| `data-invalid` | Present when invalid |
| `data-readonly` | Present when read-only |

### combobox.label

Labels the combobox. Renders a `<label>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-label` | <uid> |
| `data-readonly` | Present when read-only |
| `data-disabled` | Present when disabled |
| `data-invalid` | Present when invalid |
| `data-required` | Present when required |
| `data-focus` | Present when focused |

### combobox.control

Groups the input and its trigger/clear buttons. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-control` | <uid> |
| `data-state` | "open" \| "closed" |
| `data-focus` | Present when focused |
| `data-disabled` | Present when disabled |
| `data-invalid` | Present when invalid |

### combobox.input

The text input used to search and select from the collection. Renders an `<input>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-input` | <uid> |
| `data-invalid` | Present when invalid |
| `data-autofocus` |  |
| `data-state` | "open" \| "closed" |

### combobox.clearTrigger

Clears the current input value and selection. Renders a `<button>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-clear-trigger` | <uid> |
| `data-invalid` | Present when invalid |

### combobox.trigger

Opens and closes the combobox listbox. Not focusable unless `focusable` is set, or `popupType` is `dialog`. Renders a `<button>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `focusable` | `boolean` | No | `-` | Whether the trigger can receive focus. By default only the trigger of a combobox with `popupType` `dialog` can - otherwise the input is the focus target and the trigger stays out of the tab order. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-trigger` | <uid> |
| `data-state` | "open" \| "closed" |
| `data-invalid` | Present when invalid |
| `data-focusable` |  |
| `data-readonly` | Present when read-only |
| `data-disabled` | Present when disabled |

### combobox.positioner

Positions the floating combobox content. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-positioner` | <uid> |
| `data-nested` | Present when the positioner is nested within another {{widget}} |
| `data-has-nested` | Present when the positioner has nested {{widget}}s |

### combobox.content

The popup surface, positioned by `positioner`. Holds the `list` and `empty` and, with `popupType` `dialog`, the `input` too. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-content` | <uid> |
| `data-state` | "open" \| "closed" |
| `data-placement` | The placement of the content |
| `data-side` | The side of the trigger that the content is positioned on |
| `data-empty` | Present when the content is empty |
| `data-nested` | listbox |
| `data-has-nested` | listbox |

### combobox.list

The listbox inside `content` that holds the options - it scrolls and carries `role="listbox"`. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-list` | <uid> |
| `data-empty` | Present when the content is empty |

### combobox.empty

Displays a placeholder for when no items match the current search. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

### combobox.item

Represents a selectable option. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `item` | `mixed` | No | `-` | The collection item for this combobox option. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-item` | <uid> |
| `data-highlighted` | Present when highlighted |
| `data-state` | "checked" \| "unchecked" |
| `data-disabled` | Present when disabled |
| `data-value` | The value of the item |

### combobox.itemText

Displays the text content of an option. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-item-text` | <uid> |
| `data-state` | "checked" \| "unchecked" |
| `data-disabled` | Present when disabled |
| `data-highlighted` | Present when highlighted |

### combobox.itemIndicator

Displays the selected-state indicator for an option. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-item-indicator` | <uid> |
| `data-state` | "checked" \| "unchecked" |

### combobox.itemGroup

Groups related options together. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `value` | `string` | No | `-` | The value identifier of the item group. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-combobox-item-group` | <uid> |
| `data-empty` | Present when the content is empty |

### combobox.itemGroupLabel

Labels a group of related options. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

### combobox.hiddenInput

Provides the real, submittable value(s) for the selected item(s), since the visible input only ever holds label text. Renders one `<input>` element per selected value, or one empty `<input>` while nothing is selected.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |

### Machine JavaScript API

| Name | Type | Description |
| --- | --- | --- |
| `focused` | `boolean` | Whether the combobox is focused |
| `open` | `boolean` | Whether the combobox is open |
| `inputValue` | `string` | The value of the combobox input |
| `highlightedValue` | `string \| null` | The value of the highlighted item |
| `highlightedItem` | `V \| null` | The highlighted item |
| `setHighlightValue` | `(value: string) => void` | The value of the combobox input |
| `clearHighlightValue` | `VoidFunction` | Function to clear the highlighted value |
| `syncSelectedItems` | `VoidFunction` | Function to sync the selected items with the value. Useful when `value` is updated from async sources. |
| `selectedItems` | `V[]` | The selected items |
| `hasSelectedItems` | `boolean` | Whether there's a selected item |
| `value` | `string[]` | The selected item keys |
| `valueAsString` | `string` | The string representation of the selected items |
| `selectValue` | `(value: string) => void` | Function to select a value |
| `setValue` | `(value: string[]) => void` | Function to set the value of the combobox |
| `clearValue` | `(value?: string) => void` | Function to clear the value of the combobox |
| `focus` | `VoidFunction` | Function to focus on the combobox input |
| `setInputValue` | `(value: string, reason?: InputValueChangeReason) => void` | Function to set the input value of the combobox |
| `getItemState` | `(props: ItemProps) => ItemState` | Returns the state of a combobox item |
| `setOpen` | `(open: boolean, reason?: OpenChangeReason) => void` | Function to open or close the combobox |
| `collection` | `ListCollection<V>` | Function to toggle the combobox |
| `reposition` | `(options?: Partial<PositioningOptions>) => void` | Function to set the positioning options |
| `multiple` | `boolean` | Whether the combobox allows multiple selections |
| `disabled` | `boolean` | Whether the combobox is disabled |
| `getRootState` | `() => RootState` | Returns the state of the root |
| `getContentState` | `() => ContentState` | Returns the state of the content |
| `getTriggerState` | `(props?: TriggerProps) => TriggerState` | Returns the state of the trigger |

### Accessibility

| Key | Description |
| --- | --- |
| `ArrowDown`  | When the combobox is closed, opens the listbox and highlights to the first option. When the combobox is open, moves focus to the next option. |
| `ArrowUp`  | When the combobox is closed, opens the listbox and highlights to the last option. When the combobox is open, moves focus to the previous option. |
| `Home`  | When the combobox is open, moves focus to the first option. |
| `End`  | When the combobox is open, moves focus to the last option. |
| `Escape`  | Closes the listbox. |
| `Enter`  | Selects the highlighted option and closes the combobox. |
| `Esc`  | Closes the combobox |
