Combobox
A picker that filters a list and remembers the selected item. The field has no border and rests on the color wash unless inline. Hover and focus only shift lightness. A highlighted row shows the accent dot instead of a filled bar. Chips rest on the wash. Labels are Regular. Empty, status, and group labels are Light.
Usage guidelines
- Combobox is a filterable Select: Use Combobox when the input is restricted to a set of predefined selectable items, similar to Select but whose items are filterable. Prefer Combobox over Select when the list is large enough to warrant filtering.
- Avoid for simple search widgets: Combobox does not allow free-form text. For search widgets, use Autocomplete instead.
- Avoid when not rendering an input: Use Select if no input is rendered. Select includes accessibility features specific to a listbox without an input.
- Accessible name: Form controls must have an accessible name. Label
UiComboboxInputwith a native<label>,UiComboboxLabel, or Field.UiComboboxLabellabelsUiComboboxTriggerand is intended for the input-inside-popup pattern, where the trigger is the form control.
Anatomy
<UiComboboxRoot :items="items">
<UiComboboxLabel />
<UiComboboxInputGroup>
<UiComboboxInput />
<UiComboboxTrigger />
<UiComboboxIcon />
<UiComboboxClear />
<UiComboboxValue />
<UiComboboxChips>
<UiComboboxChip>
<UiComboboxChipRemove />
</UiComboboxChip>
</UiComboboxChips>
</UiComboboxInputGroup>
<UiComboboxPortal>
<UiComboboxBackdrop />
<UiComboboxPositioner>
<UiComboboxPopup>
<UiComboboxStatus />
<UiComboboxEmpty />
<UiComboboxList>
<UiComboboxRow>
<UiComboboxItem>
<UiComboboxItemIndicator />
</UiComboboxItem>
</UiComboboxRow>
<UiComboboxSeparator />
<UiComboboxGroup>
<UiComboboxGroupLabel />
<UiComboboxCollection />
</UiComboboxGroup>
</UiComboboxList>
</UiComboboxPopup>
</UiComboboxPositioner>
</UiComboboxPortal>
</UiComboboxRoot>
Item values
Pass the item itself as value on each UiComboboxItem. That item is what value, defaultValue, and @update:value receive.
<UiComboboxList v-slot="{ items }">
<UiComboboxItem v-for="item in items" :key="item.id" :value="item">
{{ item.label }}
</UiComboboxItem>
</UiComboboxList>
itemToStringValue (or label / value / title / name on the object) is used for the input text and the default filter. To store IDs instead, see Value selection with IDs.
Examples
Typed wrapper component
Wrap Root with a generic so items, value, and multiple stay typed:
import type { BaseComboboxFilter } from '~/lib/ui/combobox'
export type MyComboboxProps<Value, Multiple extends boolean = false> = {
items: Value[]
multiple?: Multiple
value?: Multiple extends true ? Value[] : Value | null
filter?: BaseComboboxFilter | null
}
Value selection with IDs
When the app stores IDs rather than item objects, pass the IDs as items and map each ID to a label with itemToStringValue. Put the ID on UiComboboxItem value, not the source object. defaultValue / value then receive the ID.
const fruitName = (id: unknown) => fruits.find((fruit) => fruit.id === id)?.name ?? String(id)
<UiComboboxRoot :items="fruitIds" :item-to-string-value="fruitName" default-value="banana">
<UiComboboxList v-slot="{ items }">
<UiComboboxItem v-for="id in items" :key="id" :value="id">
{{ fruitName(id) }}
</UiComboboxItem>
</UiComboboxList>
</UiComboboxRoot>
Multiple select
multiple on Root allows more than one selected item. Render chips with UiComboboxChip inside the field; Backspace on an empty input removes the last chip.
<UiComboboxRoot :items="langs" multiple>
<UiComboboxInputGroup>
<UiComboboxValue v-slot="{ items }">
<UiComboboxChips>
<UiComboboxChip v-for="item in items" :key="item.id" :value="item">
{{ item.value }}
<UiComboboxChipRemove />
</UiComboboxChip>
<UiComboboxInput placeholder="e.g. TypeScript" />
</UiComboboxChips>
</UiComboboxValue>
</UiComboboxInputGroup>
</UiComboboxRoot>
Input inside popup
UiComboboxInput can live inside UiComboboxPopup to make a searchable select. The trigger is the form control; UiComboboxLabel labels that trigger. UiComboboxValue shows the selection and UiComboboxIcon the caret.
<UiComboboxRoot :items="countries">
<UiComboboxLabel>Country</UiComboboxLabel>
<UiComboboxTrigger>
<UiComboboxValue placeholder="Select country" />
<UiComboboxIcon />
</UiComboboxTrigger>
<UiComboboxPortal>
<UiComboboxPositioner>
<UiComboboxPopup>
<UiComboboxInput placeholder="e.g. United Kingdom" />
<!-- list -->
</UiComboboxPopup>
</UiComboboxPositioner>
</UiComboboxPortal>
</UiComboboxRoot>
Grouped
Organize related options with UiComboboxGroup and UiComboboxGroupLabel. Groups are { value, items }[]. An extra property such as value is the heading when rendering the group label.
const groups = [
{ value: 'Fruits', items: ['Apple', 'Banana', 'Orange'] },
{ value: 'Vegetables', items: ['Carrot', 'Lettuce', 'Spinach'] },
]
<UiComboboxList v-slot="{ items }">
<UiComboboxGroup v-for="group in items" :key="group.value" :items="group.items">
<UiComboboxGroupLabel>{{ group.value }}</UiComboboxGroupLabel>
<UiComboboxCollection v-slot="{ items: groupItems }">
<UiComboboxItem v-for="item in groupItems" :key="item.id" :value="item" />
</UiComboboxCollection>
</UiComboboxGroup>
</UiComboboxList>
Async search (single)
Load items from a remote source by fetching on input changes (filter null). Keep the selected item in items so its label stays available while new results stream in. Put loading, errors, and hints in UiComboboxStatus. Type will_error to see the error state.
Async search (multiple)
The same remote pattern with multiple. Selected people stay in the list while new matches stream in.
Creatable
When the filter matches nothing, offer a “Create …” item. Confirming opens a Dialog to add the label, then selects it.
Virtualized
Large lists can own scrolling with native overflow (max-height on UiComboboxList). This demo renders 1000 items without an extra virtualization library.
API reference
Root
Groups all parts. Renders a <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
items | unknown[] | Group[] | [] | Flat items or groups with an items array. |
itemToStringValue | (item) => string | label / value / title / name | String used for the input, chips, and the default filter. |
filter | ((item, query, itemToString?) => boolean) | null | includes | Custom matcher. null disables internal filtering (async search). |
defaultValue | unknown | unknown[] | null / [] | Uncontrolled selection. |
value | unknown | unknown[] | — | Controlled selection (v-model:value). |
defaultInputValue | string | selected label | Uncontrolled input text. |
inputValue | string | — | Controlled input (v-model:inputValue). |
defaultOpen | boolean | false | Uncontrolled popup. |
open | boolean | — | Controlled popup (v-model:open). |
multiple | boolean | false | Allow more than one selected item. Keeps the popup open on select. |
disabled | boolean | false | Ignore interaction. Sets data-disabled. |
readOnly | boolean | false | Visible but not changeable. Sets data-readonly. |
| Event | Type | Description |
|---|---|---|
@update:value | (value) => void | Selection changed. |
@update:open | (open: boolean) => void | Popup opened or closed. |
@update:inputValue | (value: string) => void | Input text changed. |
| Attribute | Description |
|---|---|
data-open | Present when the popup is open. |
data-closed | Present when the popup is closed. |
data-disabled | Present when disabled. |
data-readonly | Present when read-only. |
data-multiple | Present when multiple is set. |
Label
Labels the trigger (input-inside-popup) or a sibling input via for. Renders a <label>.
InputGroup
Lays out the field, chips, and actions. Registers as the popup anchor. 14rem × 2rem, rounded, no border. Hover and focus shift the wash. data-multiple grows to 16rem and wraps chips.
| Attribute | Description |
|---|---|
data-popup-open | Present when the popup is open. |
data-disabled | Present when disabled. |
data-readonly | Present when read-only. |
data-multiple | Present when multiple is set. |
data-has-clear | Present when Clear is visible. |
Input
Combobox field. Renders an <input>. Arrow keys move the highlight, Enter selects, Escape closes. Backspace on an empty multiple input removes the last chip.
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | — | Placeholder text. |
id | string | — | Native id. |
| Attribute | Description |
|---|---|
data-popup-open | Present when the popup is open. |
data-list-empty | Present when there are no visible items. |
data-disabled | Present when disabled. |
data-readonly | Present when read-only. |
Trigger
Button that opens and closes the popup. Renders a <button>. Inside InputGroup it is the caret. As the form control (input inside popup) it shows Value and Icon.
| Attribute | Description |
|---|---|
data-popup-open | Present when the popup is open. |
data-disabled | Present when disabled. |
data-readonly | Present when read-only. |
data-placeholder | Present when nothing is selected. |
Clear
Clears the selection and query. Hidden when empty.
| Attribute | Description |
|---|---|
data-disabled | Present when disabled. |
data-readonly | Present when read-only. |
Icon
Decorative caret next to the trigger in the input-inside-popup pattern.
Value
Current selection. Default slot is the label string; scoped slot receives { value, items }. placeholder is shown when nothing is selected.
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | — | Shown when the selection is empty. |
Chips
Wraps selected chips and the input in multiple mode.
Chip
One selected item in multiple mode.
| Prop | Type | Default | Description |
|---|---|---|---|
value | unknown | — | The selected item this chip represents. |
ChipRemove
Removes that chip. Renders a <button>.
Portal
Teleports to body while the popup is present.
| Prop | Type | Default | Description |
|---|---|---|---|
hidden | boolean | — | Skip rendering even while present. |
| Attribute | Description |
|---|---|
data-open | Present when the popup is open. |
data-closed | Present while present but not open. |
data-starting-style | Present while animating in. |
data-ending-style | Present while animating out. |
Backdrop
Optional overlay behind the popup. Pointer-events none in this port.
Positioner
Pins the popup to the input, input group, or trigger with position: fixed. Sets --anchor-width, --available-width, --available-height, and --transform-origin.
| Prop | Type | Default | Description |
|---|---|---|---|
sideOffset | number | 4 | Gap below the anchor. |
align | 'start' | 'end' | 'start' | Align to the start or end of the anchor. |
Popup
Popup surface. Enter and leave use data-starting-style / data-ending-style (scale 0.95, 100ms). No animation library. No drop-shadow.
| Attribute | Description |
|---|---|
data-open | Present when open. |
data-closed | Present when closed. |
data-empty | Present when the list is empty. |
data-starting-style | Present while animating in. |
data-ending-style | Present while animating out. |
List
Listbox. Scoped slot exposes the filtered items (flat or grouped). Max height min(22.5rem, var(--available-height)).
| Attribute | Description |
|---|---|
data-empty | Present when there are no visible items. |
Item
One option. Renders role="option". data-highlighted shows the accent dot. data-selected when chosen.
| Prop | Type | Default | Description |
|---|---|---|---|
value | unknown | — | The item this option represents. |
ItemIndicator
Check mark on the selected item. Renders a <span>. Hidden while unselected.
Group
Section of items. Renders role="group".
| Prop | Type | Default | Description |
|---|---|---|---|
items | unknown[] | — | Items for Collection. |
GroupLabel
Heading for a group.
Collection
Passes the current group’s items to its scoped slot.
Row
A grid row (role="row") when laying items out in columns.
Status
Live region for loading, errors, or result counts.
Empty
Shown when the filter matches nothing.
Separator
Visual divider inside the popup.