# Combobox

**An input with a popup listbox 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 listbox with keyboard navigation
- Supports single and multiple selection
- Supports disabled items and item groups
- Works with the Field component for forms and validation
- Submits the selected item's `value` on form submit, not its label - the visible input only ever displays text, `hiddenInput` carries the real value(s), or an empty one while nothing is selected
- Supports an `empty` state placeholder shown automatically whenever no items match
- Filtering is plain `onInputValueChange` userland code, not a special API - swap the match logic per instance the same way you'd swap it in any Zag/Ark UI consumer
- Ships with locale-aware substring filtering via Zag's i18n utilities, wired up in the default mount entry
- Supports async, server-rendered search results via `ui:template`
- Can move the input into the popup, behind a focusable trigger, with the `dialog` popup type

## Anatomy

Unlike `Select`, Zag's combobox machine renders no native form control of its own - the visible `input` only ever holds the highlighted item's label (or, with `allowCustomValue`, arbitrary typed text), never its `value`. `hiddenInput` fills that gap: one visually hidden text input (kept out of the accessibility tree and tab order via `aria-hidden`/`tabindex="-1"`, not `type="hidden"`) per selected value, kept in sync with the current selection so the item's `value` (not its label) is what actually gets submitted. Include it once anywhere inside `combobox.root` - it renders as many hidden inputs as there are selected values (one, or - with `multiple` - several), and a single empty one while nothing is selected, like a native input would submit its name with an empty value. That empty input is also what a [Field](/docs/components/field.md) moves the focus through when the combobox is the first invalid field of a submitted form: it hands the focus on to the visible input.

Zag puts the listbox semantics and scrolling on `list`, so the options belong inside `list` rather than directly inside `content`, which is a plain wrapper around it (a dialog, with the `dialog` popup type). The styled `ui:combobox.content` is the `positioner` and `content` in one part, and `ui:combobox.list` is the `list` - you place it inside the content yourself.

`combobox.empty` renders a placeholder for when the collection has no items to show - whether that's because nothing matches the current search text, or because an async search hasn't returned results yet. It's shown/hidden automatically alongside `content`/`list`'s own `data-empty` attribute, so no wiring is needed beyond placing it next to `combobox.list` inside `content` - it isn't an option, so it doesn't belong inside the listbox.

```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

Set an initial selected value and render its label into the input on first paint.

**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

Mark specific options as unavailable.

**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

Organize items into labeled groups.

**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

Use the combobox inside `Field` to share label, name, required and invalid state.

**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

Allow users to select multiple values from the 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}" 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" />
```

### Custom Filter Logic

Override the match logic per instance by passing your own `onInputValueChange` to a custom mount entry - swap `contains` for `startsWith`, or filter however you like.

**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

Load items from a server-side search endpoint as the user types, instead of rendering the full collection up front.

Author the item's markup once inside a `ui:template` block - it makes `ui:ref` work on plain, hand-authored elements even though they're technically slot content, not a component's own template body. `combobox.item`, `combobox.itemText`, and `combobox.itemIndicator` all detect they're inside a `ui:template` block automatically - so we dont need to pass a `value` prop. On the client, clone the template per search result, populate its `ui:ref`'d elements directly, and rebuild the collection. Fetching, debouncing, and race-condition handling are left to your own code, typically built on `@zag-js/async-list`.

`collection` can be omitted entirely for a combobox with no server-known items at all - it's optional and defaults to empty regardless of `searchUrl`.

The status placeholder itself is just `combobox.empty` - `Combobox` already shows/hides it automatically whenever there are no items rendered, whatever the reason (no query typed yet, a request in flight, a failed request, or a query with zero matches). Only its _content_ - a spinner and a status text - is something the example's own code owns and updates; the loading/error messaging is entirely up to you. Both are plain, hand-authored elements passed `{ui:ref(name: 'statusSpinner', context: 'combobox')}` - since they're slot content rather than a component's own template body, `ui:ref` needs the explicit `context` argument to know which ancestor component to attach to, the same way `ui:template`'s own `context` argument does for the item markup below. The example below sends the search query via [`extbase.post()`](/docs/client-utilities/extbase.md) rather than a GET param, sidestepping a `cHash` mismatch `f:uri.action`'s URL would otherwise hit, and drives the spinner/status text off a single [`DelayedIndicator`](/docs/client-utilities/delayed-indicator.md) so they can never disagree.

**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

Async results can be grouped too - author a second `ui:template` for the group wrapper (`combobox.itemGroup`/`combobox.itemGroupLabel`), clone one per group returned by your search, and append the item clones into it instead of directly into `combobox.list`.

`combobox.itemGroup` needs a unique `data-id` per instance so `Combobox` can tell groups apart - the same thing `ui:id()` gives a server-rendered group, done client-side with `uid()`. Nothing about `Combobox`'s own rendering needed to change for this: it already looks up every `itemGroup` part independently and reads its `data-id` fresh on every render, whether that element was server-rendered or just cloned.

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

```

### Input in Content

By default the input is the combobox's focus target and the popup is a passive listbox. Set `popupType` to `PopupType::Dialog` (`popupType="{f:constant(name: 'Jramke\FluidPrimitives\Enum\PopupType::Dialog')}"`) to turn it into a popover-style picker instead: the input moves into the `content`, above the `list`, and a trigger opens the popup. The `content` is then announced as a dialog, a click on the label focuses the trigger, and the trigger becomes focusable - it is the only way in, so Zag makes it so by default (the trigger's own `focusable` prop overrides that either way).

Since the input is no longer in the control, the trigger has to show the current selection itself - here a plain button with a hand-authored `valueText` ref that the example's entry file keeps in sync with `api.valueAsString`.

**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

Default combobox trigger labels are shipped via XLF and follow the current Site Language. For per-template overrides, pass translated strings through the `translations` prop.

```html
<f:variable
    name="comboboxTranslations"
    value="{
        triggerLabel: '{f:translate(key: \'LLL:EXT:site_package/Resources/Private/Language/locallang.xlf:combobox.trigger\')}',
        clearTriggerLabel: '{f:translate(key: \'LLL:EXT:site_package/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 |
