# Combobox

A text input paired with a listbox, narrowed as the user types.

Combobox pairs a text input with a listbox, so the user narrows the list by
typing and then either picks a match or keeps what they typed. Useful when
typing beats scrolling, or when the options come from a server.

::component-preview{name="ComboboxPreview"}
```vue
<template>
  <Combobox.Root
    v-model="value"
    v-model:input-value="query"
    :options="options"
  >
    <Combobox.InputGroup
      class="group border-surface rounded-component-lg has-[:focus-visible]:focus-ring data-[popup-open=true]:focus-ring relative isolate flex h-12 w-64 cursor-text items-center justify-between gap-1.5 border-2 px-3.5 outline-4 outline-transparent transition-all duration-100 ease-linear"
    >
      <label
        class="flex max-h-full w-full cursor-text flex-col-reverse justify-center gap-0 px-1 transition-all duration-100 ease-linear group-has-[:focus]:gap-1 group-has-[input:not(:placeholder-shown)]:gap-1 before:absolute before:-inset-0.5 before:-z-10 before:content-['']"
      >
        <span
          class="relative flex h-0 w-full items-end transition-all duration-100 ease-linear group-has-[:focus]:h-[0.9375rem] group-has-[input:not(:placeholder-shown)]:h-[0.9375rem]"
        >
          <Combobox.Input
            class="text-primary-solid type-component-lg -my-1.25 box-content block h-[1lh] w-full min-w-0 bg-transparent py-1.25 [line-height:normal]! transition-colors duration-100 ease-linear outline-none placeholder:text-transparent"
            placeholder=" "
          />
        </span>
        <span
          class="text-primary-muted type-component-lg group-has-[:focus]:type-component-2xs group-has-[input:not(:placeholder-shown)]:type-component-2xs flex w-full items-center [line-height:normal]! transition-all duration-100 ease-linear"
        >
          City
        </span>
      </label>

      <Combobox.Clear
        class="text-primary-muted hover:text-primary-solid flex shrink-0 items-center transition-all duration-100 ease-linear"
      >
        <svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
          <path
            d="m4.5 4.5 7 7m0-7-7 7"
            fill="none"
            stroke="currentColor"
            stroke-width="1.5"
            stroke-linecap="round"
          />
        </svg>
      </Combobox.Clear>

      <Combobox.Trigger
        class="text-primary-muted group-has-[input:not(:placeholder-shown)]:text-primary-solid flex shrink-0 items-center transition-all duration-100 ease-linear"
      >
        <svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
          <path
            d="m4 6.5 4 4 4-4"
            fill="none"
            stroke="currentColor"
            stroke-width="1.5"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
      </Combobox.Trigger>
    </Combobox.InputGroup>

    <Combobox.Content
      class="rounded-component-2xl bg-primary-inverted shadow-component-high w-(--rack-floating-anchor-width) overflow-y-auto p-1.5 backdrop-blur-[8rem]"
    >
      <Combobox.Item
        v-for="city in matches"
        :key="city"
        class="group/item rounded-component-md type-component-lg text-primary-on-subtle hover:bg-primary-subtle active:bg-primary-subtle-hover data-[highlighted=true]:bg-primary-subtle data-[disabled=true]:text-disabled-on-subtle flex h-12 w-full items-center justify-start gap-4 px-1 transition-colors duration-100 ease-linear select-none [--mirror-combobox-item-cursor:pointer]"
        :value="city"
      >
        <Combobox.ItemText
          class="inline-flex h-full min-w-0 flex-1 items-center overflow-hidden pr-1.5 pl-5.5"
        >
          <span class="truncate">{{ city }}</span>
        </Combobox.ItemText>

        <Combobox.ItemIndicator
          class="text-primary-muted group-hover/item:text-primary-solid group-data-[highlighted=true]/item:text-primary-solid flex shrink-0 items-center pr-4 leading-none transition-colors duration-100 ease-linear"
        >
          <svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
            <path
              d="m3.5 8.5 3 3 6-6.5"
              fill="none"
              stroke="currentColor"
              stroke-width="1.5"
              stroke-linecap="round"
              stroke-linejoin="round"
            />
          </svg>
        </Combobox.ItemIndicator>
      </Combobox.Item>

      <Combobox.Empty
        class="type-component-lg text-primary-muted flex h-12 items-center pl-6.5"
      >
        No matches.
      </Combobox.Empty>
    </Combobox.Content>
  </Combobox.Root>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { Combobox } from '@maas/mirror/vue'

const options = {
  modal: false,
  portal: { disabled: true },
}

const cities = ['Glasgow', 'Bath', 'Nuremberg', 'Munich', 'Rome']

const value = ref<string | null>(null)
const query = ref('')

const matches = computed(() =>
  cities.filter((city) =>
    city.toLowerCase().includes(query.value.toLowerCase())
  )
)
</script>
```
::

## Usage guidelines

- We do not filter for you. Bind the text with `v-model:input-value` and render
  your own filtered array, so a search against a server works exactly like a
  local one.
- Make sure the combobox has an accessible name, either through a
  [`Label`](/components/label) inside a [`Field`](/components/field) or an
  `aria-label` on the input.
- `Combobox.Content` teleports to the end of `<body>` unless you turn that off
  with `options.portal.disabled`. Leave it on where the field sits inside
  anything with `overflow: hidden`.
- The content resolves everything but `side` and `align` once, during setup, so
  change the rest before the popup first renders or key the root to remount it.

## Anatomy

Assemble the parts. `Combobox.Content` is one part over four boxes: the
teleport, the positioned element, the popup and the listbox. Everything you put
inside it renders in the listbox.

::component-anatomy
---
parts:
  - name: Combobox.Root
    required: true
    description: 'Renders its children plus one hidden input per selected value.'
    children:
      - name: Combobox.InputGroup
        description: 'Optional. Renders a <div> around the input and its buttons, carrying the popup state.'
        children:
          - name: Combobox.Chips
            description: 'Optional. Renders a <div> holding one chip per selection under multiple.'
            children:
              - name: Combobox.Chip
                description: 'Renders a <div> standing for one selected value.'
                children:
                  - name: Combobox.ChipRemove
                    description: 'Renders a <button> that drops its chip’s value.'
          - name: Combobox.Input
            required: true
            description: 'Renders an <input role="combobox">. Holds focus for the whole interaction.'
          - name: Combobox.Value
            description: 'Renders a <span> with the selected labels.'
          - name: Combobox.Clear
            description: 'Renders a <button>. Clears the value and the input text.'
          - name: Combobox.Trigger
            description: 'Renders a <button>. Toggles the popup without ever taking focus.'
          - name: Combobox.Icon
            description: 'Renders a <span aria-hidden="true"> for the disclosure marker.'
      - name: Combobox.Status
        description: 'Optional. Renders a <div role="status"> for loading and result counts.'
      - name: Combobox.Content
        required: true
        description: 'Renders the teleport, the floating box, the popup and the <div role="listbox">.'
        children:
          - name: Combobox.Group
            description: 'Renders a <div role="group"> around a run of items.'
            children:
              - name: Combobox.GroupLabel
                description: 'Renders a <div> and names its group.'
          - name: Combobox.Item
            required: true
            description: 'Renders a <div role="option">.'
            children:
              - name: Combobox.ItemText
                description: 'Renders a <span> and registers its text as the item’s label.'
              - name: Combobox.ItemIndicator
                description: 'Renders a <span> while the item is selected.'
          - name: Combobox.Separator
            description: 'Renders a <div role="separator"> between groups.'
          - name: Combobox.Empty
            description: 'Renders a polite live region, filled while no item is registered.'
---
::

```vue
<script setup lang="ts">
import { Combobox, Field, Label } from '@maas/mirror/vue'
</script>

<template>
  <Field.Root :options="{ name: 'city' }">
    <Label>City</Label>
    <Combobox.Root v-model="city" v-model:input-value="query">
      <Combobox.Input placeholder=" " />
      <Combobox.Clear />
      <Combobox.Trigger />
      <Combobox.Content>
        <Combobox.Item v-for="match in matches" :key="match" :value="match">
          <Combobox.ItemText>{{ match }}</Combobox.ItemText>
          <Combobox.ItemIndicator />
        </Combobox.Item>
        <Combobox.Empty>No matches.</Combobox.Empty>
      </Combobox.Content>
    </Combobox.Root>
  </Field.Root>
</template>
```

## Examples

### Filtering

Bind the text with `v-model:input-value` and render whatever your own filtering
produces.

```vue
<script setup lang="ts">
const city = ref<string | null>(null)
const query = ref('')

const matches = computed(() =>
  cities.filter((entry) =>
    entry.toLowerCase().startsWith(query.value.toLowerCase())
  )
)
</script>

<template>
  <Combobox.Root v-model="city" v-model:input-value="query">
    <Combobox.Input />
    <Combobox.Content>
      <Combobox.Item v-for="match in matches" :key="match" :value="match">
        <Combobox.ItemText>{{ match }}</Combobox.ItemText>
      </Combobox.Item>
      <Combobox.Empty>No matches.</Combobox.Empty>
    </Combobox.Content>
  </Combobox.Root>
</template>
```

Items register as they mount, so the highlight follows your filtered array: once
the highlighted item leaves the set, the highlight moves to the selected item, or
to the first enabled one while `autoHighlight` is on. `Combobox.Empty` reads the
registered items rather than your array, which keeps it right once the items are
grouped or virtualised.

### When the popup opens

A handful of options decide that, and they are the only ones `Combobox` adds
beyond `allowCustomValue`.

```vue
<template>
  <Combobox.Root
    :options="{
      openOnInput: true,
      openOnInputClick: true,
      openOnFocus: false,
      autoHighlight: true,
      clearOnEscape: false,
    }"
  />
</template>
```

`openOnInput` opens on the first keystroke, `openOnInputClick` opens on a
pointer click so an already filled input reopens without typing, and
`openOnFocus` opens as soon as the input takes focus, before the reader has
done anything. `autoHighlight` seeds the first enabled item so Enter always has
something to commit, and `clearOnEscape` lets an Escape on an already-closed
combobox empty both the value and the text.

### Custom values

By default the value has to come from the list, so text that matches nothing is
discarded on Enter. Set `allowCustomValue` and the typed text becomes the value
instead.

```vue
<template>
  <Combobox.Root v-model="tag" :options="{ allowCustomValue: true }" />
</template>
```

The input then takes `data-custom-value` until an item is picked or the combobox
is cleared, so style the difference rather than tracking it yourself.

```css
.input[data-custom-value='true'] {
  font-style: italic;
}
```

### Multiple selection

Set `options.multiple` and the value becomes an array: picking an item toggles
it, the popup stays open, and the input text is left alone so the query survives
the selection.

```vue
<script setup lang="ts">
const tags = ref<Array<string>>([])
const query = ref('')
</script>

<template>
  <Combobox.Root
    v-model="tags"
    v-model:input-value="query"
    :options="{ multiple: true }"
  />
</template>
```

The value and `options.multiple` have to agree, or the root throws
`invalid_multiple_value`.

### Chips

We find a multiple selection easier to read as chips inside the control than as
a list beside it. `Combobox.Chips` hands out one entry per selection, each
already carrying its key and its label, and `Combobox.ChipRemove` drops the
chip it sits in. Backspace on an empty input removes the last one, so the
keyboard never needs the buttons.

```vue
<script setup lang="ts">
const tags = ref<Array<string>>([])
const query = ref('')
</script>

<template>
  <Combobox.Root
    v-model="tags"
    v-model:input-value="query"
    :options="{ multiple: true }"
  >
    <Combobox.InputGroup>
      <Combobox.Chips v-slot="{ entries }">
        <Combobox.Chip
          v-for="entry in entries"
          :key="entry.key"
          :value="entry.value"
        >
          {{ entry.label }}
          <Combobox.ChipRemove :aria-label="`Remove ${entry.label}`" />
        </Combobox.Chip>
        <Combobox.Input />
      </Combobox.Chips>
      <Combobox.Clear />
      <Combobox.Trigger />
    </Combobox.InputGroup>
  </Combobox.Root>
</template>
```

The chips are not in the tab order: the input keeps focus for the whole
interaction, the way it does everywhere else in this component.

::component-preview{name="ComboboxChipsPreview"}
```vue
<template>
  <Combobox.Root
    v-model="value"
    v-model:input-value="query"
    :options="options"
  >
    <Combobox.InputGroup
      class="group border-surface rounded-component-lg has-[:focus-visible]:focus-ring data-[popup-open=true]:focus-ring relative isolate flex w-64 cursor-text items-center justify-between gap-1.5 border-2 px-3.5 py-2.5 outline-4 outline-transparent transition-all duration-100 ease-linear"
    >
      <Combobox.Chips
        v-slot="{ entries }"
        class="flex min-w-0 flex-1 flex-wrap items-center gap-1.5"
      >
        <Combobox.Chip
          v-for="entry in entries"
          :key="entry.key"
          class="rounded-component-md bg-primary-subtle text-primary-on-subtle type-component-2xs inline-flex h-7 max-w-full shrink-0 items-center gap-1 pr-1 pl-2 transition-all duration-100 ease-linear"
          :value="entry.value"
        >
          <span class="truncate">{{ entry.label }}</span>

          <Combobox.ChipRemove
            :aria-label="`Remove ${entry.label}`"
            class="text-primary-muted hover:text-primary-solid flex size-5 shrink-0 items-center justify-center transition-colors duration-100 ease-linear"
          >
            <svg class="size-3.5" viewBox="0 0 16 16" aria-hidden="true">
              <path
                d="m4.5 4.5 7 7m0-7-7 7"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
                stroke-linecap="round"
              />
            </svg>
          </Combobox.ChipRemove>
        </Combobox.Chip>

        <Combobox.Input
          class="text-primary-solid type-component-lg placeholder:text-primary-muted h-7 min-w-16 flex-1 bg-transparent [line-height:normal]! transition-colors duration-100 ease-linear outline-none"
          placeholder="Add a city"
        />
      </Combobox.Chips>

      <Combobox.Trigger
        class="text-primary-muted group-data-[popup-open=true]:text-primary-solid flex shrink-0 items-center self-start pt-1 transition-all duration-100 ease-linear"
      >
        <svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
          <path
            d="m4 6.5 4 4 4-4"
            fill="none"
            stroke="currentColor"
            stroke-width="1.5"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
      </Combobox.Trigger>
    </Combobox.InputGroup>

    <Combobox.Content
      class="rounded-component-2xl bg-primary-inverted shadow-component-high w-(--rack-floating-anchor-width) overflow-y-auto p-1.5 backdrop-blur-[8rem]"
    >
      <Combobox.Item
        v-for="city in matches"
        :key="city"
        class="group/item rounded-component-md type-component-lg text-primary-on-subtle hover:bg-primary-subtle active:bg-primary-subtle-hover data-[highlighted=true]:bg-primary-subtle flex h-12 w-full items-center justify-start gap-4 px-1 transition-colors duration-100 ease-linear select-none [--mirror-combobox-item-cursor:pointer]"
        :value="city"
      >
        <Combobox.ItemText
          class="inline-flex h-full min-w-0 flex-1 items-center overflow-hidden pr-1.5 pl-5.5"
        >
          <span class="truncate">{{ city }}</span>
        </Combobox.ItemText>

        <Combobox.ItemIndicator
          class="text-primary-muted group-hover/item:text-primary-solid group-data-[highlighted=true]/item:text-primary-solid flex shrink-0 items-center pr-4 leading-none transition-colors duration-100 ease-linear"
        >
          <svg class="size-4.5" viewBox="0 0 16 16" aria-hidden="true">
            <path
              d="m3.5 8.5 3 3 6-6.5"
              fill="none"
              stroke="currentColor"
              stroke-width="1.5"
              stroke-linecap="round"
              stroke-linejoin="round"
            />
          </svg>
        </Combobox.ItemIndicator>
      </Combobox.Item>

      <Combobox.Empty
        class="type-component-lg text-primary-muted flex h-12 items-center pl-6.5"
      >
        No matches.
      </Combobox.Empty>
    </Combobox.Content>
  </Combobox.Root>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { Combobox } from '@maas/mirror/vue'

const options = {
  modal: false,
  multiple: true,
  portal: { disabled: true },
}

const cities = ['Glasgow', 'Bath', 'Nuremberg', 'Munich', 'Rome']

const value = ref<Array<string>>([])
const query = ref('')

const matches = computed(() =>
  cities.filter((city) =>
    city.toLowerCase().includes(query.value.toLowerCase())
  )
)
</script>
```
::

### Object values

An item value does not have to be a string. Hand `Combobox.Item` the object you
already hold and tell the combobox how to read it, exactly as
[`Select`](/components/select#object-values) does.

```vue
<script setup lang="ts">
interface City {
  code: string
  name: string
}

const city = ref<City | null>(null)
</script>

<template>
  <Combobox.Root
    v-model="city"
    v-model:input-value="query"
    name="city"
    :options="{
      isItemEqualToValue: (item, value) => item.code === value.code,
      itemToStringLabel: (item) => item.name,
      itemToStringValue: (item) => item.code,
    }"
  >
    <Combobox.Input />
  </Combobox.Root>
</template>
```

`itemToStringLabel` is also the text a selection commits to the input, and
`itemToStringValue` is what the hidden inputs submit. An object shaped
`{ value, label }` needs neither.

### Announcing an empty or loading list

`Combobox.Empty` is a polite live region that stays mounted, so a filter that
finds nothing is announced rather than silently rendered. `Combobox.Status` is
the same idea for everything else the reader should hear, which for a server
list is usually the loading state.

```vue
<template>
  <Combobox.Root v-model="city" v-model:input-value="query">
    <Combobox.Input />
    <Combobox.Status v-slot="{ count }">
      {{ loading ? 'Searching' : `${count} cities` }}
    </Combobox.Status>
    <Combobox.Content>
      <Combobox.Item v-for="match in matches" :key="match" :value="match">
        <Combobox.ItemText>{{ match }}</Combobox.ItemText>
      </Combobox.Item>
      <Combobox.Empty>No matches.</Combobox.Empty>
    </Combobox.Content>
  </Combobox.Root>
</template>
```

### Inside a field

`Combobox.Input` is a field control, and `Combobox.Root` renders one hidden
input per selected value once it has a name, so
[the form sees it](/components/form-integration#form-participation).

```vue
<template>
  <Field.Root :options="{ name: 'city' }">
    <Label>City</Label>
    <Combobox.Root v-model="city" v-model:input-value="query">
      <Combobox.Input />
    </Combobox.Root>
    <Field.Error />
  </Field.Root>
</template>
```

The hidden inputs hold the value rather than the text, so what the user typed
reaches the form only once it has become a value.

### Positioning

`Combobox.Content` positions itself against the input, steered by the
`floating` keys on the root’s `options`; the options table below lists every
one of them. The resolved placement comes back
as `data-side` and `data-align`.

```vue
<template>
  <Combobox.Root
    :options="{
      floating: { side: 'top', align: 'center', sideOffset: 8, flip: false },
    }"
  />
</template>
```

Wrap the control in a `Combobox.InputGroup` and the popup anchors on the group
instead, which is what it needs when the frame the user sees is wider than the
input inside it. Pass `anchor` to position against something else again, and
pass any of the eight floating keys straight to the content to override the
options for one instance.

```vue
<template>
  <Combobox.Content :anchor="() => frame" side="top" :side-offset="12" />
</template>
```

The content writes `--rack-floating-anchor-width`,
`--rack-floating-anchor-height`, `--rack-floating-available-width`,
`--rack-floating-available-height` and `--rack-floating-transform-origin` onto
the floating box, so the popup is already as wide as the anchor and never
taller than the space around it.

```css
.popup {
  min-width: var(--mirror-combobox-popup-min-width);
  max-height: var(--mirror-combobox-popup-max-height);
}
```

### Where the popup renders

The content teleports to the end of `<body>`, which is what keeps a popup out
of an ancestor’s `overflow: hidden` and above the rest of the page. Turn it off
to render in place, or name a target of your own.

```vue
<template>
  <Combobox.Root :options="{ portal: { to: '#overlays' } }" />
  <Combobox.Root :options="{ portal: { disabled: true } }" />
</template>
```

`options.backdrop` adds a fixed `<div role="presentation">` behind the popup,
for the modal case. It has no dismiss behaviour of its own: the popup’s layer
already closes on a pointer down outside.

```vue
<template>
  <Combobox.Root :options="{ modal: true, backdrop: true }" />
</template>
```

### Reaching the combobox from anywhere

Give the root an `id` and `useMirrorCombobox(id)` opens, clears and reads that
combobox from anywhere in the app.

```vue
<script setup lang="ts">
import { useMirrorCombobox } from '@maas/mirror/vue'
import { ComboboxId } from '~/constants/comboboxIds'

const { isOpen, open, clear } = useMirrorCombobox(ComboboxId.City)
</script>

<template>
  <Button :aria-expanded="isOpen" @click="open()">Search cities</Button>
  <Button @click="clear()">Reset</Button>
</template>
```

The state is created by whichever side asks for it first, so the toolbar may
mount before the combobox does.

### Styling from state

Every part writes its state to `data-*`, so the styling is a matter of
attribute selectors.

```css
.item[data-highlighted='true'] {
  background: var(--app-color-primary-bg-subtle);
}

.item[data-disabled='true'] {
  opacity: 0.5;
}

.trigger[data-popup-open='true'] {
  transform: rotate(180deg);
}
```

The content’s four boxes are not parts, so they are reached by their
`data-scope` or by the class each one ships. A `class` on `Combobox.Content`
is applied to the popup, the box you paint.

```vue
<template>
  <Combobox.Content class="popup" />
</template>
```

```css
.mirror-combobox-floating {
  --mirror-combobox-floating-z-index: 80;
}

.popup[data-list-empty='true'] {
  min-height: 0;
}

[data-scope='list'][data-multiple='true'] {
  padding-inline-start: 0;
}
```

## API reference

**Module.** A bundled `options` object on the root, and `useMirrorCombobox(id)`
as the programmatic API. Every part that reads the combobox takes `id` to
resolve one it is not nested inside.

### `Combobox.Root`

Renders its children plus one visually hidden `<input>` per selected value.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: 'The instance ID, for [`useMirrorCombobox`](#reaching-the-combobox-from-anywhere).'
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: modelValue
        description: The selected value or values. `v-model`.
      - label: 'string | number | object | Array | undefined'
      - label: undefined
  - items:
      - label: defaultValue
        description: Initial selection when uncontrolled. Falls back to `[]` under `options.multiple`.
      - label: 'string | number | object | Array | null'
      - label: 'null'
  - items:
      - label: inputValue
        description: The text in the input. `v-model:input-value`.
      - label: 'string | undefined'
      - label: undefined
  - items:
      - label: defaultInputValue
        description: Initial text when uncontrolled.
      - label: string
      - label: ''''''
  - items:
      - label: open
        description: Open state. `v-model:open`.
      - label: 'boolean | undefined'
      - label: undefined
  - items:
      - label: defaultOpen
        description: Initial open state when uncontrolled.
      - label: boolean
      - label: 'false'
  - items:
      - label: options
        description: Everything else. Deep-merged over the defaults.
      - label: ComboboxOptions
      - label: see below
        plaintext: true
---
::

`disabled`, `readOnly`, `required` and `name` resolve in one order everywhere:
the `options` object first, then the surrounding `Field`, then `false`. Leave
the option unset and the field answers; set it, `false` included, and it wins.

Control is decided once, on the first render, for `modelValue`, `inputValue`
and `open` alike. A binding that later turns `undefined` does not hand control
back.

#### Options

`ComboboxOptions` is [`SelectOptions`](/components/select#options) without
`typeahead`, plus the flags of its own listed first below.

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: openOnInput
        description: Opens the popup on the first keystroke.
      - label: boolean
      - label: 'true'
  - items:
      - label: openOnInputClick
        description: Opens the popup on a pointer click of the input.
      - label: boolean
      - label: 'true'
  - items:
      - label: openOnFocus
        description: Opens the popup as soon as the input takes focus.
      - label: boolean
      - label: 'false'
  - items:
      - label: autoHighlight
        description: 'Seeds the highlight on [the first enabled item](#when-the-popup-opens).'
      - label: boolean
      - label: 'true'
  - items:
      - label: clearOnEscape
        description: An Escape on an already-closed combobox empties the value and the text.
      - label: boolean
      - label: 'false'
  - items:
      - label: allowCustomValue
        description: Commits the typed text as the value when nothing is highlighted.
      - label: boolean
      - label: 'false'
  - items:
      - label: multiple
        description: Turns the value into an array and keeps the popup open while picking.
      - label: boolean
      - label: 'false'
  - items:
      - label: modal
        description: Traps focus and marks the rest of the document `inert`.
      - label: boolean
      - label: 'false'
  - items:
      - label: loop
        description: Arrow keys wrap at the ends.
      - label: boolean
      - label: 'true'
  - items:
      - label: disabled
        description: Disables the input and refuses opening. Falls to the `Field` when unset.
      - label: boolean
      - label: undefined
  - items:
      - label: readOnly
        description: Blocks changes but keeps focus. Falls to the `Field` when unset.
      - label: boolean
      - label: undefined
  - items:
      - label: required
        description: Marks the hidden inputs required. Falls to the `Field` when unset.
      - label: boolean
      - label: undefined
  - items:
      - label: name
        description: Name for the hidden inputs.
      - label: string
      - label: undefined
  - items:
      - label: form
        description: ID of the form the hidden inputs belong to.
      - label: string
      - label: undefined
  - items:
      - label: isItemEqualToValue
        description: Decides whether an item value and a selected value are the same one. Object values compare by reference unless replaced.
      - label: '(item, value) => boolean'
      - label: Object.is
        plaintext: true
  - items:
      - label: itemToStringLabel
        description: 'Turns an item value into the text `Combobox.Value` shows and the input commits.'
      - label: '(item) => string'
      - label: see `Select`
        plaintext: true
  - items:
      - label: itemToStringValue
        description: 'Turns an item value into the string the hidden inputs submit.'
      - label: '(item) => string'
      - label: see `Select`
        plaintext: true
  - items:
      - label: highlightOnHover
        description: The pointer moves the highlight.
      - label: boolean
      - label: 'true'
  - items:
      - label: dismiss.escape
        description: Escape dismisses the layer.
      - label: boolean
      - label: 'true'
  - items:
      - label: dismiss.pointerDownOutside
        description: A pointer down outside closes the popup.
      - label: boolean
      - label: 'true'
  - items:
      - label: dismiss.focusOutside
        description: Focus leaving closes the popup.
      - label: boolean
      - label: 'true'
  - items:
      - label: focus.trapped
        description: Focus is trapped in the popup. Left off, focus stays in the input throughout.
      - label: boolean
      - label: 'false'
  - items:
      - label: focus.restore
        description: Closing restores focus.
      - label: boolean
      - label: 'false'
  - items:
      - label: backdrop
        description: Renders a fixed backdrop behind the popup, for the modal case.
      - label: boolean
      - label: 'false'
  - items:
      - label: portal.to
        description: Teleport target for the content.
      - label: 'string | HTMLElement'
      - label: body
  - items:
      - label: portal.disabled
        description: Renders the content in place instead of teleporting it.
      - label: boolean
      - label: 'false'
  - items:
      - label: portal.defer
        description: Resolves the target after mount, for a target rendered by the same app.
      - label: boolean
      - label: 'false'
  - items:
      - label: floating.side
        description: Preferred side. Reactive.
      - label: '''top'' | ''right'' | ''bottom'' | ''left'''
      - label: '''bottom'''
  - items:
      - label: floating.sideOffset
        description: Gap from the input, in pixels.
      - label: number
      - label: '4'
  - items:
      - label: floating.align
        description: Alignment along that side. Reactive.
      - label: '''start'' | ''center'' | ''end'''
      - label: '''start'''
  - items:
      - label: floating.alignOffset
        description: Shift along the alignment axis, in pixels.
      - label: number
      - label: '0'
  - items:
      - label: floating.strategy
        description: Positioning strategy.
      - label: '''absolute'' | ''fixed'''
      - label: '''absolute'''
  - items:
      - label: floating.flip
        description: Flip to the opposite side when there is no room.
      - label: boolean
      - label: 'true'
  - items:
      - label: floating.shift
        description: Slide along the side to stay in view.
      - label: boolean
      - label: 'true'
  - items:
      - label: floating.collisionPadding
        description: Padding used by both.
      - label: number
      - label: '8'
  - items:
      - label: forceMount.popup
        description: Keeps the popup and the backdrop mounted while closed.
      - label: boolean
      - label: 'false'
  - items:
      - label: forceMount.empty
        description: Keeps the empty state showing beside the items.
      - label: boolean
      - label: 'false'
  - items:
      - label: forceMount.clear
        description: Keeps the clear button mounted while there is nothing to clear.
      - label: boolean
      - label: 'false'
  - items:
      - label: transition.popup
        description: Vue transition name for the popup.
      - label: string
      - label: '''mirror-combobox-popup'''
  - items:
      - label: transition.backdrop
        description: Vue transition name for the backdrop.
      - label: string
      - label: '''mirror-combobox-backdrop'''
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: update:modelValue
        description: The selection changes; a clear emits `null`.
      - label: 'string | number | object | Array | null'
  - items:
      - label: update:inputValue
        description: The text changes.
      - label: string
  - items:
      - label: update:open
        description: The popup opens or closes.
      - label: boolean
  - items:
      - label: highlightChange
        description: The highlight moves, including the `null` it emits on close.
      - label: 'string | number | object | null'
---
::

A disabled combobox emits nothing, and a read-only one refuses opening,
selecting and clearing before any of them reach the state.

### `Combobox.Input`

Renders an `<input role="combobox" type="text" autocomplete="off">` with
`aria-haspopup="listbox"`, the focused element for the whole interaction. It is
a void element, so there is no default slot.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: placeholder
        description: The native placeholder.
      - label: string
      - label: undefined
  - items:
      - label: disabled
        description: Escape hatch over the options, then the `Field`. Renders the native attribute.
      - label: boolean
      - label: options.disabled
  - items:
      - label: readOnly
        description: Escape hatch over the options, then the `Field`.
      - label: boolean
      - label: options.readOnly
  - items:
      - label: required
        description: Escape hatch over the options, then the `Field`.
      - label: boolean
      - label: options.required
---
::

#### Data attributes

The [field state set](/components/styling#the-field-state-set), plus the
combobox-only attributes below.

### `Combobox.InputGroup`

Optional. Renders a `<div>` around the input and whatever sits beside it,
republishing the popup state so the whole control can be styled from one
element. It also becomes the anchor `Combobox.Content` measures.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: open
        description: Mirrors `data-popup-open`.
      - label: boolean
---
::

### `Combobox.Value`

Renders a `<span>` with the selected labels, joined with a comma. Useful beside
a `Combobox.Input` that holds a search query rather than the selection, and as
the source for a chip loop.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: placeholder
        description: 'Rendered when nothing is selected. For markup rather than a string, fill the `placeholder` slot instead.'
      - label: string
      - label: ''''''
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The selected value or values.
      - label: 'ComboboxModelValue | null'
  - items:
      - label: entries
        description: 'One `{ key, value, label }` per selection, ready for a `v-for`.'
      - label: 'Array<ComboboxValueEntry>'
  - items:
      - label: label
        description: The joined label text.
      - label: string
---
::

### `Combobox.Icon`

Renders a `<span aria-hidden="true">` with `data-popup-open`, for the disclosure
marker. No props beyond `id` and the primitive ones.

### `Combobox.Trigger`

Renders a `<button type="button" tabindex="-1" aria-hidden="true">` that toggles
the popup and hands focus straight back to the input.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: disabled
        description: Escape hatch over the options.
      - label: boolean
      - label: options.disabled
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: open
        description: Mirrors `data-popup-open`.
      - label: boolean
  - items:
      - label: pressed
        description: Mirrors `data-pressed`.
      - label: boolean
---
::

### `Combobox.Clear`

Renders a `<button type="button" tabindex="-1">` that clears the value and the
input text, then returns focus to the input. It is not rendered while there is
nothing to clear, and refuses to act while the combobox is disabled or
read-only.

One clear button per combobox, so `options.forceMount.clear` is the only home
for keeping it mounted with nothing to clear.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: visible
        description: Mirrors `data-visible`.
      - label: boolean
---
::

### `Combobox.Chips`

Renders a `<div>` holding one `Combobox.Chip` per selection, plus the input.
Only meaningful under `options.multiple`.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: entries
        description: 'One `{ key, value, label }` per selection, ready for a `v-for`.'
      - label: 'Array<ComboboxValueEntry>'
---
::

### `Combobox.Chip`

Renders a `<div>` standing for one selected value, and tells the
`Combobox.ChipRemove` inside it which value to drop.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: value
        description: Required. The selection this chip stands for.
      - label: 'string | number | object'
      - label: none
        plaintext: true
  - items:
      - label: label
        description: Overrides the label the value resolves to.
      - label: string
      - label: 'from `itemToStringLabel`'
        plaintext: true
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The chip’s value.
      - label: ComboboxValueType
  - items:
      - label: label
        description: The chip’s label.
      - label: string
---
::

### `Combobox.ChipRemove`

Renders a `<button type="button" tabindex="-1">` that drops its chip’s value and
returns focus to the input. Throws `missing_context` outside a `Combobox.Chip`.

### `Combobox.Empty`

Renders a `<div aria-live="polite" aria-atomic="true">` that stays mounted for
the whole interaction. Only its content comes and goes, and while there is
something to show it is `hidden`.

One empty state per combobox, so `options.forceMount.empty` is the only home
for filling it regardless of the open state and the item count.

### `Combobox.Status`

Renders a `<div role="status" aria-live="polite" aria-atomic="true">` for
loading text and result counts. It says nothing on its own.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: count
        description: The number of registered items.
      - label: number
  - items:
      - label: empty
        description: Mirrors `data-list-empty`.
      - label: boolean
  - items:
      - label: open
        description: Mirrors `data-popup-open`.
      - label: boolean
---
::

### `Combobox.Content`

Renders everything that floats: the teleport, the floating box, the popup and
the listbox. Everything in its default slot renders inside the listbox. It
anchors on `Combobox.InputGroup` when there is one and on `Combobox.Input`
otherwise, and a pointer down or a focus move inside the input, the group or
the trigger is not treated as an outside dismissal.

#### Layers

None of the four is a part, so each is styled through its `data-scope` or the
class it ships. A `class` on the content itself is applied to the popup.

::docs-table
---
columns:
  - label: Layer
  - label: Element
  - label: Publishes
rows:
  - items:
      - label: portal
        description: The teleport. Configured through `options.portal`.
      - label: none
        plaintext: true
      - label: nothing
        plaintext: true
  - items:
      - label: backdrop
        description: '`.mirror-combobox-backdrop`, only under `options.backdrop`.'
      - label: 'div[role=presentation]'
      - label: '`data-combobox`, `data-state`'
        plaintext: true
  - items:
      - label: floating
        description: '`.mirror-combobox-floating`. Carries the floating styles and the `--rack-floating-*` set.'
      - label: div
      - label: '`data-side`, `data-align`, `data-anchor-hidden`, `data-state`'
        plaintext: true
  - items:
      - label: popup
        description: '`.mirror-combobox-popup`. Presence, dismissal and the modal behaviour live here.'
      - label: div
      - label: '`data-combobox`, `data-side`, `data-align`, `data-state`, `data-list-empty`, `data-multiple`'
        plaintext: true
  - items:
      - label: list
        description: '`.mirror-combobox-list`. Its generated ID is the input’s `aria-controls`, and it takes `aria-multiselectable` under `options.multiple`.'
      - label: 'div[role=listbox]'
      - label: '`data-list-empty`, `data-multiple`'
        plaintext: true
---
::

#### Props

Every prop below but `anchor` is an escape hatch over an option, `forceMount`
over `options.forceMount.popup` and the rest over the matching
`options.floating` key, and only `side` and `align` stay reactive after mount.

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: forceMount
        description: Keeps the popup and the backdrop mounted while closed.
      - label: boolean
      - label: options.forceMount.popup
        plaintext: true
  - items:
      - label: anchor
        description: Position against this element instead.
      - label: 'HTMLElement | (() => HTMLElement | null)'
      - label: the input group, then the input
        plaintext: true
  - items:
      - label: side
        description: Preferred side.
      - label: '''top'' | ''right'' | ''bottom'' | ''left'''
      - label: '''bottom'''
  - items:
      - label: sideOffset
        description: Gap from the input, in pixels.
      - label: number
      - label: '4'
  - items:
      - label: align
        description: Alignment along that side.
      - label: '''start'' | ''center'' | ''end'''
      - label: '''start'''
  - items:
      - label: alignOffset
        description: Shift along the alignment axis, in pixels.
      - label: number
      - label: '0'
  - items:
      - label: strategy
        description: Positioning strategy.
      - label: '''absolute'' | ''fixed'''
      - label: '''absolute'''
  - items:
      - label: flip
        description: Flip to the opposite side when there is no room.
      - label: boolean
      - label: 'true'
  - items:
      - label: shift
        description: Slide along the side to stay in view.
      - label: boolean
      - label: 'true'
  - items:
      - label: collisionPadding
        description: Padding used by both.
      - label: number
      - label: '8'
---
::

#### Slots

::docs-table
---
columns:
  - label: Slot
  - label: Renders
rows:
  - items:
      - label: default
      - label: Inside the listbox. Items, groups, separators and the empty state.
        plaintext: true
  - items:
      - label: header
      - label: Inside the popup, above the listbox. Anything that is not an option.
        plaintext: true
  - items:
      - label: footer
      - label: Inside the popup, below the listbox.
        plaintext: true
---
::

#### Slot props

All three slots take the same props.

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: open
        description: Mirrors `data-state`.
      - label: boolean
  - items:
      - label: side
        description: Resolved after collision handling.
      - label: ComboboxSide
  - items:
      - label: align
        description: Resolved after collision handling.
      - label: ComboboxAlign
---
::

### `Combobox.Item`

Renders a `<div role="option">`. Items register in document order, so grouping
and portals cannot scramble the arrow-key sequence. Naming a `<button>` in `as`
gets the button semantics with it, including the native `disabled` attribute.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: value
        description: 'Required, and unique among the items. An object needs [`isItemEqualToValue`](#object-values).'
      - label: 'string | number | object'
      - label: none
        plaintext: true
  - items:
      - label: label
        description: The label committed to the input on selection. Falls back to the item’s rendered text, then to `options.itemToStringLabel`.
      - label: string
      - label: the `Combobox.ItemText` content
        plaintext: true
  - items:
      - label: disabled
        description: Skipped by the arrow keys and unselectable.
      - label: boolean
      - label: 'false'
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: selected
        description: Mirrors `data-selected`.
      - label: boolean
  - items:
      - label: highlighted
        description: Mirrors `data-highlighted`.
      - label: boolean
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: boolean
  - items:
      - label: index
        description: Mirrors `data-index`.
      - label: number
---
::

### `Combobox.Group`

Renders a `<div role="group">` around a run of items, pointing `aria-labelledby`
at the `Combobox.GroupLabel` inside it. No props beyond the primitive ones.

### `Combobox.GroupLabel`

Renders a `<div>` and names its group. Throws `missing_context` outside a
`Combobox.Group`.

### `Combobox.Separator`

Renders a `<div role="separator">` between groups.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: orientation
        description: Sets `aria-orientation`.
      - label: '''horizontal'' | ''vertical'''
      - label: '''horizontal'''
---
::

### `Combobox.ItemText`

Renders a `<span>` and registers its text as the item’s label when no `label`
prop was given.

### `Combobox.ItemIndicator`

Renders a `<span aria-hidden="true">` while the item is selected.

#### Props

One indicator per item, so `forceMount` is configured on the part and nowhere
else. Set it on the indicator you want kept in the tree while its item is
unselected. Every other indicator is left where it was.

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: forceMount
        description: Keeps it mounted while unselected.
      - label: boolean
      - label: 'false'
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: selected
        description: Mirrors `data-selected`.
      - label: boolean
---
::

### Composable

`useMirrorCombobox(id)` reaches a combobox from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: isOpen
        description: '`true` while the popup is open.'
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: open
        description: Opens the popup and seeds the highlight. Does nothing while disabled or read-only.
      - label: () => void
        escape: true
  - items:
      - label: close
        description: Closes the popup and drops the highlight. `restoreFocus` acts only with `options.focus.restore` on.
      - label: '(restoreFocus?: boolean) => void'
        escape: true
  - items:
      - label: toggle
        description: Opens or closes.
      - label: () => void
        escape: true
  - items:
      - label: clear
        description: Empties the value and the input text. Does nothing while disabled or read-only.
      - label: () => void
        escape: true
  - items:
      - label: value
        description: The selected value, or the array of them.
      - label: 'ComputedRef<ComboboxModelValue | null>'
        escape: true
  - items:
      - label: setValue
        description: Sets the selection and leaves the input text alone.
      - label: '(next: ComboboxModelValue | null) => void'
        escape: true
  - items:
      - label: inputValue
        description: The text in the input.
      - label: ComputedRef<string>
        escape: true
  - items:
      - label: setInputValue
        description: Sets the text. `customValue` defaults to `true` and drives `data-custom-value`.
      - label: '(next: string, customValue?: boolean) => void'
        escape: true
  - items:
      - label: highlightedValue
        description: The highlighted item’s value.
      - label: 'ComputedRef<ComboboxValueType | null>'
        escape: true
---
::

### Data attributes

An input has a native placeholder, so `data-placeholder` from `Select` never
appears here.

::data-attributes
::

### CSS variables

::css-variables
::

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_combobox_context
        description: A part rendered outside `Combobox.Root` and received no `id`.
  - items:
      - label: missing_combobox_input
        description: The content opened with no `Combobox.Input` registered.
  - items:
      - label: duplicate_combobox_input
        description: A second `Combobox.Input` registered in the same combobox.
  - items:
      - label: missing_context
        description: 'A `Combobox.ItemText` or `Combobox.ItemIndicator` rendered outside a `Combobox.Item`, a `Combobox.GroupLabel` outside a `Combobox.Group`, or a `Combobox.ChipRemove` outside a `Combobox.Chip`. A rack-level code, described in [TypeScript](/components/typescript#errors).'
  - items:
      - label: missing_item_value
        description: A `Combobox.Item` rendered without a `value`.
  - items:
      - label: duplicate_item_value
        description: Two items in the same combobox declared the same `value`.
  - items:
      - label: invalid_multiple_value
        description: '`options.multiple` is set and the value is not an array, or is unset and the value is.'
---
::

## Accessibility

The input takes `role="combobox"`, `aria-autocomplete="list"`, `aria-expanded`,
`aria-controls` and `aria-activedescendant`, and focus never leaves it, so every
key below is handled there and all of them are ignored while the combobox is
disabled.

::docs-table
---
columns:
  - label: Key
  - label: Behaviour
rows:
  - items:
      - label: Printable characters
        plaintext: true
      - label: Native. Types into the input, and `openOnInput` opens the popup on the resulting change. A pointer click opens it too, under `openOnInputClick`.
        plaintext: true
  - items:
      - label: '`ArrowDown` / `ArrowUp`'
        plaintext: true
      - label: 'Opens the popup while it is closed: `ArrowDown` lands on the first item and `ArrowUp` on the last. While it is open, moves the highlight.'
        plaintext: true
  - items:
      - label: '`PageDown` / `PageUp`'
        plaintext: true
      - label: Moves the highlight ten items while the popup is open. Clamps at the ends whatever `loop` says.
        plaintext: true
  - items:
      - label: '`Home` / `End`'
        plaintext: true
      - label: Not bound. Native caret movement in the input.
        plaintext: true
  - items:
      - label: Enter
      - label: While open, selects the highlighted item. With `allowCustomValue` and nothing highlighted, commits the typed text. While closed it is native, so the form submits.
        plaintext: true
  - items:
      - label: Escape
      - label: Closes the popup. With `clearOnEscape`, an Escape on an already-closed combobox clears the value and the text.
        plaintext: true
  - items:
      - label: Tab
      - label: Commits the highlight the way Enter does, without preventing the default.
        plaintext: true
  - items:
      - label: Backspace
      - label: Native while there is text. Under `options.multiple` on an empty input it drops the last selection instead, which is the chip nearest the caret.
        plaintext: true
---
::

Every keyboard move scrolls the highlighted item into view, so a popup capped by
`--mirror-combobox-popup-max-height` cannot lose the highlight off screen. A
pointer highlight leaves the scroll position where the reader put it.

Typing goes into the input rather than into a typeahead search, which is why
there is no `typeahead` option here. The trigger, the chips and the items
suppress `pointerdown`, so clicking any of them never pulls focus out of the
input.

`Combobox.Empty` and `Combobox.Status` are both polite live regions, and they
divide the work: `Empty` announces that a filter found nothing, `Status`
announces whatever else the consumer wants said, such as a loading state or a
result count.
