# Combobox

> A text input that filters a list of options as you type.

## Examples

### Basic

```vue
<script setup lang="ts">
const country = ref<string>();
const countries = [
  "Argentina",
  "Australia",
  "Brazil",
  "Canada",
  "Egypt",
  "France",
  "Germany",
  "Ghana",
  "India",
  "Japan",
  "Kenya",
  "Mexico",
  "Nigeria",
  "Norway",
  "Portugal",
  "South Africa",
  "Spain",
  "Sweden",
  "United Kingdom",
  "United States",
].map((label) => ({ label, value: label.toLowerCase().replaceAll(" ", "-") }));
</script>

<template>
  <div class="demo-field">
    <UiCombobox
      v-model="country"
      :options="countries"
      label="Country"
      placeholder="Search countries"
    />
  </div>
</template>
```

### Tags

With `multiple`, picks show as chips. With `creatable`, Enter on new text emits `create` so you can add it.

```vue
<script setup lang="ts">
const tags = ref(["vue", "css"]);
const options = ref(
  ["vue", "nuxt", "css", "typescript", "accessibility", "ssr"].map((value) => ({
    label: value,
    value,
  })),
);

function create(tag: string) {
  const value = tag.toLowerCase();
  options.value.push({ label: value, value });
  tags.value.push(value);
}
</script>

<template>
  <div class="demo-field">
    <UiCombobox
      v-model="tags"
      :options="options"
      label="Tags"
      placeholder="Add a tag"
      hint="Enter adds a new tag. Backspace removes the last."
      multiple
      creatable
      @create="create"
    />
  </div>
</template>
```

### Async options

Fetch matches as `v-model:query` changes, and set `ignore-filter` so your results show as they are. Set `loading` while a request runs.

```vue
<script setup lang="ts">
const query = ref("");
const user = ref<number>();

const people = [
  "Ada Lovelace",
  "Alan Turing",
  "Grace Hopper",
  "Katherine Johnson",
  "Linus Torvalds",
  "Margaret Hamilton",
];

// Stand-in for a search request: options arrive already filtered.
const { data: results, pending } = useAsyncData(
  "people-search",
  async () => {
    await new Promise((resolve) => setTimeout(resolve, 400));
    const q = query.value.toLowerCase();
    return people
      .map((label, value) => ({ label, value }))
      .filter((person) => person.label.toLowerCase().includes(q));
  },
  { watch: [query], default: () => [] },
);
</script>

<template>
  <div class="demo-field">
    <UiCombobox
      v-model="user"
      v-model:query="query"
      :options="results"
      ignore-filter
      :loading="pending"
      label="Assignee"
      placeholder="Search people"
    >
      <template #empty>No one matches “{{ query }}”.</template>
    </UiCombobox>
  </div>
</template>
```

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `SelectOption<T>[]` | `[]` | Options to choose from. |
| `label` | `string` | — | Visible label, linked to the input. |
| `hint` | `string` | — | Help text under the control, announced as its description. |
| `error` | `string \| boolean` | — | `true` marks the control invalid; a string also shows it as the message, replacing the hint. |
| `placeholder` | `string` | — | Input placeholder. |
| `multiple` | `boolean` | `false` | Pick several; `v-model` becomes an array, shown as chips. Backspace removes the last one. |
| `creatable` | `boolean` | `false` | Enter on text matching no option emits `create` with it. |
| `optional` | `boolean` | `false` | Appends the `optional` translation to the label. |
| `disabled` | `boolean` | `false` | Disables the control. |
| `filter` | `ComboboxFilter<T>` | — | Custom match for each option against the lower-cased query. By default, labels are ranked exact, then prefix, then substring. |
| `ignoreFilter` | `boolean` | `false` | Show `options` as given, without filtering, e.g. results from a search request. |
| `loading` | `boolean` | `false` | Options are being fetched: shows the `loading` translation and marks the list busy. |
| `name` | `string` | — | Form field name: submits the value with a native form, via hidden inputs. |
| `id` | `string` | — | Id for the input; generated with `useId()` when omitted. |
| `modelValue` | `T \| T[]` | — | The selected value, or values with `multiple`. |
| `query` | `string` | `""` | The input's text. Bind it to fetch options as the user types. |

| Event | Payload | Description |
| --- | --- | --- |
| `create` | `[query: string]` | Enter was pressed on text matching no option (with `creatable`). |
| `update:modelValue` | `[value: T \| T[] \| undefined]` | `v-model:modelValue` update. |
| `update:query` | `[value: string]` | `v-model:query` update. |

| Slot | Props | Description |
| --- | --- | --- |
| `option` | `{ option: SelectOption<T>; selected: boolean; active: boolean; }` | Option content. |
| `chip` | `{ option: SelectOption<T>; remove: () => void; }` | Each selected value in `multiple` mode. Call `remove` to deselect it. |
| `empty` | `{ query: string; }` | Shown when nothing matches. |
| `create` | `{ query: string; }` | Shown with `creatable` when Enter would create the typed value. |
| `loading` | — | Shown while `loading`. |

Types:

```ts
export interface SelectOption<T extends SelectValue = SelectValue> {
  label: string;
  value: T;
  disabled?: boolean;
}

export type ComboboxFilter<T extends SelectValue> = (
  option: SelectOption<T>,
  query: string,
) => boolean;

export type SelectValue = string | number;
```

## Styling

- Parts (`[data-part="…"]`): `field`, `field-label`, `field-optional`, `combobox-control`, `combobox-chip`, `combobox-chip-label`, `combobox-chip-remove`, `combobox-input`, `combobox-popup`, `combobox-listbox`, `combobox-option`, `combobox-option-label`, `combobox-loading`, `combobox-create`, `combobox-empty`, `combobox-status`, `field-message`
- State attributes: `data-field-error`, `data-field-name`, `data-combobox-open`, `data-combobox-disabled`, `data-combobox-active`, `data-field-tone`

## Keyboard

| Keys        | Action                                                   |
| ----------- | -------------------------------------------------------- |
| Typing      | Filter, and open the list                                |
| `↓` / `↑`   | Move between options, wrapping                           |
| `Alt` + `↓` | Open without moving                                      |
| `Enter`     | Select the active option, or create (with `creatable`)   |
| `Escape`    | Close; press again to clear                              |
| `Backspace` | With `multiple` and an empty input, remove the last chip |

## Accessibility

- Focus stays in the input; screen readers follow the highlighted option through `aria-activedescendant`.
- While typing, a hidden status announces the number of matches, “No results”, or “Loading…”.
- Chips aren't in the tab order, so with `multiple` the picks are read as part of the input's description.
- In single mode, leaving the input with unmatched text restores the selection.
