Skip to content

Autocomplete

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

View source View as Markdown

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.

Start typing to search
App.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-[content-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-[content-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, listEmpty, contentOpen }"
        class="type-component-2xs text-primary-muted px-1"
      >
        <template v-if="contentOpen">
          <template v-if="listEmpty">No matching fonts</template>
          <span v-else class="tabular-nums">{{ count }} fonts</span>
        </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/Autocomplete'

const options = {
  initial: { highlight: true },
  openOnInputClick: true,
  teleport: { 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.should
  • Make sure the autocomplete has an accessible name, either through a Label inside a Field or an aria-label on the input.must
  • 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.must
  • Autocomplete.Content teleports to the end of <body> unless you turn that off with options.teleport.disabled. Leave it on where the field sits inside anything with overflow: hidden.should
  • 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.must