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 UiComboboxInput with a native <label>, UiComboboxLabel, or Field. UiComboboxLabel labels UiComboboxTrigger and 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>.

PropTypeDefaultDescription
itemsunknown[] | Group[][]Flat items or groups with an items array.
itemToStringValue(item) => stringlabel / value / title / nameString used for the input, chips, and the default filter.
filter((item, query, itemToString?) => boolean) | nullincludesCustom matcher. null disables internal filtering (async search).
defaultValueunknown | unknown[]null / []Uncontrolled selection.
valueunknown | unknown[]—Controlled selection (v-model:value).
defaultInputValuestringselected labelUncontrolled input text.
inputValuestring—Controlled input (v-model:inputValue).
defaultOpenbooleanfalseUncontrolled popup.
openboolean—Controlled popup (v-model:open).
multiplebooleanfalseAllow more than one selected item. Keeps the popup open on select.
disabledbooleanfalseIgnore interaction. Sets data-disabled.
readOnlybooleanfalseVisible but not changeable. Sets data-readonly.
EventTypeDescription
@update:value(value) => voidSelection changed.
@update:open(open: boolean) => voidPopup opened or closed.
@update:inputValue(value: string) => voidInput text changed.
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-disabledPresent when disabled.
data-readonlyPresent when read-only.
data-multiplePresent 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.

AttributeDescription
data-popup-openPresent when the popup is open.
data-disabledPresent when disabled.
data-readonlyPresent when read-only.
data-multiplePresent when multiple is set.
data-has-clearPresent 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.

PropTypeDefaultDescription
placeholderstring—Placeholder text.
idstring—Native id.
AttributeDescription
data-popup-openPresent when the popup is open.
data-list-emptyPresent when there are no visible items.
data-disabledPresent when disabled.
data-readonlyPresent 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.

AttributeDescription
data-popup-openPresent when the popup is open.
data-disabledPresent when disabled.
data-readonlyPresent when read-only.
data-placeholderPresent when nothing is selected.

Clear

Clears the selection and query. Hidden when empty.

AttributeDescription
data-disabledPresent when disabled.
data-readonlyPresent 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.

PropTypeDefaultDescription
placeholderstring—Shown when the selection is empty.

Chips

Wraps selected chips and the input in multiple mode.

Chip

One selected item in multiple mode.

PropTypeDefaultDescription
valueunknown—The selected item this chip represents.

ChipRemove

Removes that chip. Renders a <button>.

Portal

Teleports to body while the popup is present.

PropTypeDefaultDescription
hiddenboolean—Skip rendering even while present.
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent while present but not open.
data-starting-stylePresent while animating in.
data-ending-stylePresent 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.

PropTypeDefaultDescription
sideOffsetnumber4Gap below the anchor.
align'start' | 'end''start'Align to the start or end of the anchor.

Popup surface. Enter and leave use data-starting-style / data-ending-style (scale 0.95, 100ms). No animation library. No drop-shadow.

AttributeDescription
data-openPresent when open.
data-closedPresent when closed.
data-emptyPresent when the list is empty.
data-starting-stylePresent while animating in.
data-ending-stylePresent while animating out.

List

Listbox. Scoped slot exposes the filtered items (flat or grouped). Max height min(22.5rem, var(--available-height)).

AttributeDescription
data-emptyPresent when there are no visible items.

Item

One option. Renders role="option". data-highlighted shows the accent dot. data-selected when chosen.

PropTypeDefaultDescription
valueunknown—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".

PropTypeDefaultDescription
itemsunknown[]—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.