# Autocomplete

A text input that suggests as you type, where the text is the value.

Autocomplete is a text field that offers suggestions while the user types, and
the text in the field is the value. Reach for it when anything the user writes
is acceptable and the list is there to save typing. When only a value from the
list will do, that is a [`Combobox`](/components/combobox).

::component-preview{name="AutocompletePreview"}
```vue
<template>
  <div class="flex w-64 flex-col gap-1.5">
    <Autocomplete.Root v-model="value" :items="fonts" :options="options">
      <Autocomplete.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-full 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]"
          >
            <Autocomplete.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"
          >
            Font
          </span>
        </label>

        <Autocomplete.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>
        </Autocomplete.Clear>

        <Autocomplete.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"
        >
          <Autocomplete.Icon
            class="flex items-center transition-transform duration-100 ease-linear data-[popup-open=true]:rotate-180"
          >
            <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>
          </Autocomplete.Icon>
        </Autocomplete.Trigger>
      </Autocomplete.InputGroup>

      <Autocomplete.Status
        v-slot="{ count, empty, open }"
        class="type-component-2xs text-primary-muted px-1 tabular-nums"
      >
        <template v-if="open">
          {{ empty ? 'No matching fonts' : `${count} fonts` }}
        </template>
        <template v-else>Start typing to search</template>
      </Autocomplete.Status>

      <Autocomplete.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]"
      >
        <Autocomplete.Collection v-slot="{ item, value: font }">
          <Autocomplete.Item
            class="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 px-5.5 transition-colors duration-100 ease-linear select-none [--mirror-autocomplete-item-cursor:pointer]"
            :value="font"
          >
            <span class="truncate">{{ item }}</span>
          </Autocomplete.Item>
        </Autocomplete.Collection>

        <Autocomplete.Empty
          class="type-component-lg text-primary-muted flex h-12 items-center pl-5.5"
        >
          No matching fonts.
        </Autocomplete.Empty>
      </Autocomplete.Content>
    </Autocomplete.Root>
  </div>
</template>

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

const options = {
  autoHighlight: true,
  openOnInputClick: true,
  portal: { disabled: true },
  floating: { sideOffset: 28 },
}

const fonts = [
  'Alte Haas Grotesk',
  'Basis Grotesque',
  'Bely Display',
  'Cardinal Fruit',
  'Domaine Text',
  'Editorial New',
  'Fraunces',
  'Gambetta',
  'Instrument Serif',
  'Louize',
]

const value = ref('')
</script>
```
::

## Usage guidelines

- Hand the root an `items` array and it filters for you. Pass `filter: null`
  when the narrowing already happened somewhere else, on a server for instance,
  and render whatever you were given.
- Make sure the autocomplete has an accessible name, either through a
  [`Label`](/components/label) inside a [`Field`](/components/field) or an
  `aria-label` on the input.
- `Autocomplete.Status` is a live region, so keep it mounted outside the popup.
  A region that appears together with its first text is announced unreliably.
- `Autocomplete.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. `Autocomplete.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: Autocomplete.Root
    required: true
    description: 'Renders its children. Owns the text, the source items and the filtering.'
    children:
      - name: Autocomplete.InputGroup
        description: 'Optional. Renders a <div> around the input and its buttons, carrying the popup state.'
        children:
          - name: Autocomplete.Input
            required: true
            description: 'Renders an <input role="combobox">. Holds focus for the whole interaction.'
      - name: Autocomplete.Clear
        description: 'Renders a <button>. Empties the text.'
      - name: Autocomplete.Trigger
        description: 'Renders a <button>. Toggles the popup without ever taking focus.'
        children:
          - name: Autocomplete.Icon
            description: 'Renders a <span aria-hidden="true"> that follows the open state.'
      - name: Autocomplete.Value
        description: 'Renders a <span> holding the current text.'
      - name: Autocomplete.Status
        description: 'Renders a <div role="status"> announcing how many items are left.'
      - name: Autocomplete.Content
        required: true
        description: 'Renders the teleport, the floating box, the popup and the <div role="listbox">.'
        children:
          - name: Autocomplete.Collection
            description: 'Renders nothing. Walks the filtered items through a scoped slot.'
            children:
              - name: Autocomplete.Item
                required: true
                description: 'Renders a <div role="option">.'
          - name: Autocomplete.Group
            description: 'Renders a <div role="group"> holding one group’s items.'
            children:
              - name: Autocomplete.GroupLabel
                description: 'Renders a <div> and names the group.'
          - name: Autocomplete.Separator
            description: 'Renders a <div role="separator">.'
          - name: Autocomplete.Row
            description: 'Renders a <div role="row"> for a grid layout.'
          - name: Autocomplete.Empty
            description: 'Renders a <div> while the popup is open and no item is registered.'
---
::

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

<template>
  <Field.Root :options="{ name: 'font' }">
    <Label>Font</Label>
    <Autocomplete.Root v-model="font" :items="fonts">
      <Autocomplete.Input placeholder=" " />
      <Autocomplete.Clear />
      <Autocomplete.Trigger>
        <Autocomplete.Icon />
      </Autocomplete.Trigger>
      <Autocomplete.Status v-slot="{ count }">{{ count }} fonts</Autocomplete.Status>
      <Autocomplete.Content>
        <Autocomplete.Collection v-slot="{ item, value }">
          <Autocomplete.Item :value="value">{{ item }}</Autocomplete.Item>
        </Autocomplete.Collection>
        <Autocomplete.Empty>No matching fonts.</Autocomplete.Empty>
      </Autocomplete.Content>
    </Autocomplete.Root>
  </Field.Root>
</template>
```

## Examples

### The four modes

`options.mode` decides two things at once: whether the list narrows as the user
types, and whether the highlighted item is written back into the input.

::docs-table
---
columns:
  - label: Mode
  - label: The list
  - label: The input
rows:
  - items:
      - label: '''list'''
        description: The default.
      - label: Narrows as the query changes.
        plaintext: true
      - label: Only ever what the user typed.
        plaintext: true
  - items:
      - label: '''both'''
      - label: Narrows as the query changes.
        plaintext: true
      - label: Completed from the highlighted item.
        plaintext: true
  - items:
      - label: '''inline'''
      - label: Stays whole.
        plaintext: true
      - label: Completed from the highlighted item.
        plaintext: true
  - items:
      - label: '''none'''
      - label: Stays whole.
        plaintext: true
      - label: Only ever what the user typed.
        plaintext: true
---
::

```vue
<template>
  <Autocomplete.Root v-model="font" :items="fonts" :options="{ mode: 'both' }" />
</template>
```

Under `both` and `inline` the highlighted item’s label goes into the field while
the query stays what the user typed, and the part they did not type is left
selected, so the next keystroke replaces it. The input takes `data-completing`
for as long as that lasts, and moving the highlight away or pressing Escape puts
the typed text back.

```css
.input[data-completing='true'] {
  color: var(--app-color-primary-fg-muted);
}
```

### Filtering

The default filter keeps every item whose text contains the query, ignoring case
and accents, so “jose” finds “José”. Replace it with your own to match
differently.

```vue
<template>
  <Autocomplete.Root
    :items="fonts"
    :options="{
      filter: (item, query, toStringValue) =>
        toStringValue(item).toLowerCase().startsWith(query.toLowerCase()),
    }"
  />
</template>
```

Set `filter: null` when the items arrive already narrowed, from a server for
instance, and the root renders them untouched. `limit` caps how many items make
it through, so a long list never paints in full.

Items can be anything, not just strings. `itemToStringValue` is how the filter,
the default label and `Autocomplete.Collection` read one.

```vue
<template>
  <Autocomplete.Root
    :items="fonts"
    :options="{ itemToStringValue: (item) => item.family }"
  />
</template>
```

### Grouping

A grouped source is an array of objects that each carry their own `items`. The
outer `Autocomplete.Collection` walks the groups, the inner one walks the items
of the group it sits in, and a group with nothing left after filtering is
dropped rather than rendered as a bare heading.

```vue
<script setup lang="ts">
const fonts = [
  { value: 'Sans', items: ['Alte Haas Grotesk', 'Basis Grotesque'] },
  { value: 'Serif', items: ['Bely Display', 'Cardinal Fruit'] },
]
</script>

<template>
  <Autocomplete.Content>
    <Autocomplete.Collection v-slot="{ item: group, index }">
      <Autocomplete.Separator v-if="index > 0" />
      <Autocomplete.Group :items="group.items">
        <Autocomplete.GroupLabel>{{ group.value }}</Autocomplete.GroupLabel>
        <Autocomplete.Collection v-slot="{ item, value }">
          <Autocomplete.Item :value="value">{{ item }}</Autocomplete.Item>
        </Autocomplete.Collection>
      </Autocomplete.Group>
    </Autocomplete.Collection>
  </Autocomplete.Content>
</template>
```

### Telling the user what happened

`Autocomplete.Status` is a polite live region with the registered item count in
its slot. Write the sentence yourself, because only you know the noun. Nothing
is registered while the popup is closed, so read `open` as well or a field
nobody has touched reads as a field with no matches.

```vue
<template>
  <Autocomplete.Status v-slot="{ count, empty, open }">
    <template v-if="open">
      {{ empty ? 'No matching fonts' : `${count} fonts` }}
    </template>
    <template v-else>Start typing to search</template>
  </Autocomplete.Status>
</template>
```

`Autocomplete.Empty` covers the same state visually, inside the popup, and reads
the registered items rather than your array, which keeps it right once the items
are grouped or virtualised.

### When the popup opens

Typing always opens it. Beyond that there are two switches.

```vue
<template>
  <Autocomplete.Root
    :options="{ openOnInputClick: true, autoHighlight: true }"
  />
</template>
```

`openOnInputClick` opens the popup when the user clicks into a field that
already has text, and `autoHighlight` seeds the first enabled item so Enter
always has something to commit. Both are off by default, because a seeded
highlight lets Enter commit an item the user never chose.

### Inside a field

`Autocomplete.Input` is a field control, and a native one, so the form reads it
directly and no hidden input is needed.

```vue
<template>
  <Field.Root :options="{ name: 'font' }">
    <Label>Font</Label>
    <Autocomplete.Root v-model="font" :items="fonts">
      <Autocomplete.Input />
    </Autocomplete.Root>
    <Field.Error />
  </Field.Root>
</template>
```

The field’s `disabled`, `readOnly`, `required` and `name` reach the input unless
the root or the input overrides them, and the validity comes back out as
`data-valid` and `data-invalid`.

### Positioning

`Autocomplete.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>
  <Autocomplete.Root
    :options="{
      floating: { side: 'top', align: 'center', sideOffset: 8, flip: false },
    }"
  />
</template>
```

Wrap the control in an `Autocomplete.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>
  <Autocomplete.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-autocomplete-popup-min-width);
  max-height: var(--mirror-autocomplete-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>
  <Autocomplete.Root :options="{ portal: { to: '#overlays' } }" />
  <Autocomplete.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>
  <Autocomplete.Root :options="{ modal: true, backdrop: true }" />
</template>
```

### Reaching the autocomplete from anywhere

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

```vue
<script setup lang="ts">
import { useMirrorAutocomplete } from '@maas/mirror/vue'
import { AutocompleteId } from '~/constants/autocompleteIds'

const { isOpen, open, clear } = useMirrorAutocomplete(AutocompleteId.Font)
</script>

<template>
  <Button :aria-expanded="isOpen" @click="open()">Search fonts</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 autocomplete 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;
}

.icon[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
`Autocomplete.Content` is applied to the popup, the box you paint.

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

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

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

[data-scope='list'][data-grid='true'] {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}
```

## API reference

**Module.** A bundled `options` object on the root, and
`useMirrorAutocomplete(id)` as the programmatic API. Every part takes `id` to
resolve an autocomplete it is not nested inside.

### `Autocomplete.Root`

Renders its children. It owns the text, the source items and the filtering, and
renders no element of its own.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: 'The instance ID, for [`useMirrorAutocomplete`](#reaching-the-autocomplete-from-anywhere).'
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: modelValue
        description: The text in the input, which is the value. `v-model`.
      - label: 'string | undefined'
      - label: undefined
  - items:
      - label: defaultValue
        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: items
        description: 'The suggestions, flat or [grouped](#grouping).'
      - label: AutocompleteSource
      - label: '[]'
  - items:
      - label: options
        description: Everything else. Deep-merged over the defaults.
      - label: AutocompleteOptions
      - 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.

#### Options

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: mode
        description: 'Which of [the four modes](#the-four-modes) the list and the input follow.'
      - label: '''list'' | ''both'' | ''inline'' | ''none'''
      - label: '''list'''
  - items:
      - label: filter
        description: 'Keeps or drops one item; `null` [hands the filtering to you](#filtering).'
      - label: '((item, query, itemToStringValue) => boolean) | null'
      - label: contains
        plaintext: true
  - items:
      - label: itemToStringValue
        description: Reads one item as text. Strings answer with themselves, objects with their `label` or `value`.
      - label: '(item: unknown) => string'
      - label: see description
        plaintext: true
  - items:
      - label: limit
        description: Most items to render. `-1` renders all of them.
      - label: number
      - label: '-1'
  - items:
      - label: grid
        description: 'Renders the list as `role="grid"`, for items laid out in rows.'
      - label: boolean
      - label: 'false'
  - items:
      - label: openOnInputClick
        description: A click in the input opens the popup.
      - label: boolean
      - label: 'false'
  - items:
      - label: autoHighlight
        description: Seeds the highlight on the first enabled item.
      - 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: Refuses typing, opening and clearing, and keeps focus. Falls to the `Field` when unset.
      - label: boolean
      - label: undefined
  - items:
      - label: required
        description: Marks the input required. Falls to the `Field` when unset.
      - label: boolean
      - label: undefined
  - items:
      - label: name
        description: Name for the input. Falls to the `Field` when unset.
      - label: string
      - label: undefined
  - 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 the popup and the control 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-autocomplete-popup'''
  - items:
      - label: transition.backdrop
        description: Vue transition name for the backdrop.
      - label: string
      - label: '''mirror-autocomplete-backdrop'''
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: update:modelValue
        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 | null'
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: items
        description: 'The filtered source, or its groups when the source is [grouped](#grouping).'
      - label: AutocompleteSource
        escape: true
  - items:
      - label: value
        description: The text in the input.
      - label: string
---
::

### `Autocomplete.Input`

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

#### 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
autocomplete-only attributes below.

### `Autocomplete.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 `Autocomplete.Content` measures.

#### Slot props

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

### `Autocomplete.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: disabled
        description: Mirrors `data-disabled`.
      - label: boolean
---
::

### `Autocomplete.Clear`

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

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

### `Autocomplete.Value`

Renders a `<span>` holding the current text. The default slot content is that
text, so an empty part is already useful.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The text in the input.
      - label: string
---
::

### `Autocomplete.Icon`

Renders a `<span aria-hidden="true">` carrying `data-popup-open`, for the
chevron that turns.

#### Slot props

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

### `Autocomplete.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 `Autocomplete.InputGroup` when there is one and on
`Autocomplete.Input` otherwise.

#### 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-autocomplete-backdrop`, only under `options.backdrop`.'
      - label: 'div[role=presentation]'
      - label: '`data-autocomplete`, `data-state`'
        plaintext: true
  - items:
      - label: floating
        description: '`.mirror-autocomplete-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-autocomplete-popup`. Presence, dismissal and the modal behaviour live here.'
      - label: div
      - label: '`data-autocomplete`, `data-side`, `data-align`, `data-state`, `data-list-empty`'
        plaintext: true
  - items:
      - label: list
        description: '`.mirror-autocomplete-list`. Its generated ID is the input’s `aria-controls`.'
      - label: 'div[role=listbox], role=grid under `options.grid`'
      - label: '`data-grid`, `data-list-empty`'
        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, rows, 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: AutocompleteSide
  - items:
      - label: align
        description: Resolved after collision handling.
      - label: AutocompleteAlign
  - items:
      - label: items
        description: The filtered source.
      - label: AutocompleteSource
        escape: true
  - items:
      - label: grouped
        description: The source is an array of groups.
      - label: boolean
---
::

### `Autocomplete.Collection`

Renders nothing of its own and walks the filtered items through its scoped slot.
Inside an `Autocomplete.Group` it walks that group’s items instead, which is how
one component covers both levels of a grouped source.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: item
        description: The source entry, untouched.
      - label: unknown
  - items:
      - label: value
        description: '`itemToStringValue(item)`. Pass it straight to `Autocomplete.Item`.'
      - label: string
  - items:
      - label: index
        description: Position within this collection.
      - label: number
---
::

### `Autocomplete.Item`

Renders a `<div role="option">`. Items register in document order, so grouping,
rows and portals cannot scramble the arrow-key sequence.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: value
        description: Required, and unique among the items.
      - label: 'string | number'
      - label: none
        plaintext: true
  - items:
      - label: label
        description: The text committed to the input on selection. Falls back to the item’s rendered text, then to `String(value)`.
      - label: string
      - label: the rendered text
        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
---
::

### `Autocomplete.Group`

Renders a `<div role="group">` labelled by its `Autocomplete.GroupLabel`, and
holds the items a nested `Autocomplete.Collection` walks.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: items
        description: This group’s items, as they came out of the filter.
      - label: 'Array<unknown>'
      - label: '[]'
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: items
        description: The same array.
      - label: 'Array<unknown>'
        escape: true
---
::

### `Autocomplete.GroupLabel`

Renders a `<div>` and names the group it sits in. Throws
`missing_autocomplete_group` outside one.

### `Autocomplete.Separator`

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

#### Props

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

### `Autocomplete.Row`

Renders a `<div role="row">`, for items laid out in a grid under
`options.grid`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: index
        description: Position of the row, counting from zero. Rendered as `aria-rowindex`, which counts from one.
      - label: number
      - label: undefined
---
::

### `Autocomplete.Status`

Renders a `<div role="status" aria-live="polite" aria-atomic="true">` holding
whatever sentence you write from the item count.

#### Slot props

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

### `Autocomplete.Empty`

Renders a `<div role="presentation">` while the popup is open and no
`Autocomplete.Item` is registered.

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

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: query
        description: The text the user typed, the one that matched no items.
      - label: string
---
::

### Composable

`useMirrorAutocomplete(id)` reaches an autocomplete 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 text. Does nothing while disabled or read-only.
      - label: () => void
        escape: true
  - items:
      - label: value
        description: The text in the input.
      - label: ComputedRef<string>
        escape: true
  - items:
      - label: setValue
        description: Sets the text, and the query with it, so the list re-filters.
      - label: '(next: string) => void'
        escape: true
  - items:
      - label: query
        description: The text the user typed. It only differs from `value` while a completion is showing.
      - label: ComputedRef<string>
        escape: true
  - items:
      - label: highlightedValue
        description: The highlighted item’s value.
      - label: 'ComputedRef<AutocompleteValueType | null>'
        escape: true
  - items:
      - label: filteredItems
        description: The filtered source.
      - label: ComputedRef<AutocompleteSource>
        escape: true
---
::

### Data attributes

::data-attributes
::

### CSS variables

::css-variables
::

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_autocomplete_context
        description: A part rendered outside `Autocomplete.Root` and received no `id`.
  - items:
      - label: missing_autocomplete_input
        description: The content opened with no `Autocomplete.Input` registered.
  - items:
      - label: duplicate_autocomplete_input
        description: A second `Autocomplete.Input` registered in the same autocomplete.
  - items:
      - label: missing_autocomplete_group
        description: An `Autocomplete.GroupLabel` rendered outside an `Autocomplete.Group`.
  - items:
      - label: missing_item_value
        description: An `Autocomplete.Item` rendered without a `value`.
  - items:
      - label: duplicate_item_value
        description: Two items in the same autocomplete declared the same `value`.
---
::

## Accessibility

The input takes `role="combobox"`, `aria-autocomplete`, `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 autocomplete is
disabled. `aria-controls` is only present while the list is, so it never points
at an element that has been unmounted.

::docs-table
---
columns:
  - label: Key
  - label: Behaviour
rows:
  - items:
      - label: Printable characters
        plaintext: true
      - label: Native. Types into the input and opens the popup, and the list narrows under `list` and `both`.
        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. Under `both` and `inline` the move writes the highlighted item into the input.'
        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: Moves the highlight to the first or last enabled item while the popup is open. Native caret movement while it is closed.
        plaintext: true
  - items:
      - label: Enter
      - label: While open, fills the input from the highlighted item and closes. With nothing highlighted it closes and the typed text stands. While closed it is native, so the form submits.
        plaintext: true
  - items:
      - label: Escape
      - label: Closes the popup and puts the typed text back over any completion showing.
        plaintext: true
  - items:
      - label: Tab
      - label: Closes the popup and moves on, without preventing the default. Nothing is committed.
        plaintext: true
---
::

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