Autocomplete

An autocomplete combines a select with filtering, allowing users to search and select from a list of options

Usage

<script setup lang="ts">
import { Autocomplete } from '@hareui/vue'
</script>
States to Visit
Select states
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, TagGroup } from '@hareui/vue'import { ref } from 'vue'const items: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]const selectedKeys = ref<string[]>([])function onRemoveTags(ids: string[]) {  selectedKeys.value = selectedKeys.value.filter(key => !ids.includes(key))}</script><template>  <Autocomplete    v-model="selectedKeys"    class="w-[256px]"    label="States to Visit"    placeholder="Select states"    selection-mode="multiple"    search-placeholder="Search..."    :items="items"  >    <template #value="{ selectedItems, isPlaceholder, text }">      <template v-if="isPlaceholder">        {{ text }}      </template>      <TagGroup v-else size="sm" removable aria-label="Selected" :items="selectedItems.map(item => ({ id: String(item.value), label: item.label }))" @remove="onRemoveTags" />    </template>    <template #empty>      No results found    </template>  </Autocomplete></template>

Anatomy

Autocomplete is a Select whose popover starts with a search field that filters the options. It renders a Label, a trigger showing the selected value, a popover holding a SearchField-styled input and a ListBox-styled option list, and an optional Description / FieldError. Options use ListBox's items format, including sections and separators.

<template>
  <Autocomplete label="State" :items="items" clearable>
    <template #label />                                  <!-- Label content -->
    <template #value="{ selectedItems, isPlaceholder, text }" /> <!-- trigger value -->
    <template #clear />                                  <!-- clear button content (with `clearable`) -->
    <template #indicator="{ open }" />                   <!-- trigger indicator (default: chevron) -->
    <template #description />                            <!-- Description content -->
    <template #error="validation" />                     <!-- FieldError content -->
    <template #search-trailing="{ inputValue }" />       <!-- after the search input -->
    <template #item-leading="{ item, selected }" />      <!-- ListBox item slots -->
    <template #item-indicator="{ selected }" />          <!-- option checkmark -->
    <template #section-label="{ section }" />            <!-- section header -->
    <template #empty />                                  <!-- no matching option -->
    <template #loading />                                <!-- with `loading`: after the last option -->
  </Autocomplete>
</template>
ui keydata-slotElement
baseautocompleteroot <div>
labellabelLabel
triggerautocomplete-trigger<div role="group"> holding the value, clear button and indicator button
valueautocomplete-value<span> inside the trigger
clearButtonautocomplete-clear-button<button> (with clearable)
indicatorautocomplete-indicator / autocomplete-default-indicatorchevron
popoverautocomplete-popoverpopover
searchField, searchGroup, searchIcon, searchInput, searchClearButtonsearch-field*the search field (with search)
listBoxlist-boxthe option list

Typing items

items takes the exported ListBoxItem type (ListBoxEntry when it mixes in sections and separators). Extra fields are kept: extend the type, and the slots receive your type, so no casts are needed.

import type { ListBoxItem } from '@hareui/vue'

interface MyItem extends ListBoxItem {
  icon: string
}

const items: MyItem[] = [/* ... */]
// <template #item-leading="{ item }"> — item.icon is a string

Examples

Variants

The Autocomplete component supports two visual variants:

  • primary (default) - Standard styling with shadow, suitable for most use cases
  • secondary - Lower emphasis variant without shadow, suitable for use in Surface components

Single Select Variants

Primary variant
Select one
Secondary variant
Select one

Multiple Select Variants

Primary variant
Select multiple
Secondary variant
Select multiple
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, TagGroup } from '@hareui/vue'import { ref } from 'vue'const items: ListBoxItem[] = [  { value: 'option1', label: 'Option 1' },  { value: 'option2', label: 'Option 2' },  { value: 'option3', label: 'Option 3' },  { value: 'option4', label: 'Option 4' },]const selectedKey1 = ref<string | null>(null)const selectedKey2 = ref<string | null>(null)const selectedKeys1 = ref<string[]>([])const selectedKeys2 = ref<string[]>([])function onRemoveTags1(ids: string[]) {  selectedKeys1.value = selectedKeys1.value.filter(key => !ids.includes(key))}function onRemoveTags2(ids: string[]) {  selectedKeys2.value = selectedKeys2.value.filter(key => !ids.includes(key))}</script><template>  <div class="flex flex-col gap-8">    <div class="flex flex-col gap-4">      <h3 class="text-lg font-semibold">        Single Select Variants      </h3>      <div class="flex flex-col gap-4">        <Autocomplete          v-model="selectedKey1"          clearable          class="w-[256px]"          label="Primary variant"          placeholder="Select one"          variant="primary"          search-placeholder="Search..."          :items="items"        >          <template #empty>            No results found          </template>        </Autocomplete>        <Autocomplete          v-model="selectedKey2"          clearable          class="w-[256px]"          label="Secondary variant"          placeholder="Select one"          variant="secondary"          search-placeholder="Search..."          :items="items"        >          <template #empty>            No results found          </template>        </Autocomplete>      </div>    </div>    <div class="flex flex-col gap-4">      <h3 class="text-lg font-semibold">        Multiple Select Variants      </h3>      <div class="flex flex-col gap-4">        <Autocomplete          v-model="selectedKeys1"          clearable          class="w-[256px]"          label="Primary variant"          placeholder="Select multiple"          selection-mode="multiple"          variant="primary"          search-placeholder="Search..."          :items="items"        >          <template #value="{ selectedItems, isPlaceholder, text }">            <template v-if="isPlaceholder">              {{ text }}            </template>            <TagGroup v-else size="sm" removable aria-label="Selected" :items="selectedItems.map(item => ({ id: String(item.value), label: item.label }))" @remove="onRemoveTags1" />          </template>          <template #empty>            No results found          </template>        </Autocomplete>        <Autocomplete          v-model="selectedKeys2"          clearable          class="w-[256px]"          label="Secondary variant"          placeholder="Select multiple"          selection-mode="multiple"          variant="secondary"          search-placeholder="Search..."          :items="items"        >          <template #value="{ selectedItems, isPlaceholder, text }">            <template v-if="isPlaceholder">              {{ text }}            </template>            <TagGroup v-else size="sm" variant="surface" removable aria-label="Selected" :items="selectedItems.map(item => ({ id: String(item.value), label: item.label }))" @remove="onRemoveTags2" />          </template>          <template #empty>            No results found          </template>        </Autocomplete>      </div>    </div>  </div></template>

Full Width

State
Select one
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, Surface } from '@hareui/vue'import { ref } from 'vue'const items: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]const selectedKey = ref<string | null>(null)</script><template>  <Surface class="w-[380px] space-y-4 rounded-3xl p-6">    <Autocomplete      v-model="selectedKey"      clearable      full-width      label="State"      placeholder="Select one"      variant="secondary"      search-label="Search states"      search-placeholder="Search states..."      :items="items"    >      <template #empty>        No results found      </template>    </Autocomplete>  </Surface></template>

With Description

State
Select one
Select your state of residence
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete } from '@hareui/vue'const items: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]</script><template>  <Autocomplete    clearable    class="w-[256px]"    label="State"    placeholder="Select one"    search-label="Search states"    search-placeholder="Search states..."    description="Select your state of residence"    :items="items"  >    <template #empty>      No results found    </template>  </Autocomplete></template>

Required

State
Select one
Country
Select a country
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, Button, Form } from '@hareui/vue'const states: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]const countries: ListBoxItem[] = [  { value: 'usa', label: 'United States' },  { value: 'canada', label: 'Canada' },  { value: 'mexico', label: 'Mexico' },  { value: 'uk', label: 'United Kingdom' },  { value: 'france', label: 'France' },  { value: 'germany', label: 'Germany' },]function onSubmit(event: SubmitEvent) {  const data = Object.fromEntries(new FormData(event.currentTarget as HTMLFormElement))  console.log('Form submitted:', data)  alert('Form submitted successfully!')}</script><template>  <Form class="flex w-[256px] flex-col gap-4" @submit.prevent="onSubmit">    <Autocomplete      required      clearable      class="w-full"      name="state"      label="State"      placeholder="Select one"      search-label="Search states"      search-placeholder="Search states..."      :items="states"    >      <template #empty>        No results found      </template>    </Autocomplete>    <Autocomplete      required      clearable      class="w-full"      name="country"      label="Country"      placeholder="Select a country"      search-label="Search countries"      search-placeholder="Search countries..."      :items="countries"    >      <template #empty>        No results found      </template>    </Autocomplete>    <Button type="submit">      Submit    </Button>  </Form></template>

Disabled

State
California
Countries to Visit
Argentina, Japan, and France
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete } from '@hareui/vue'const items: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]const countries: ListBoxItem[] = [  { value: 'argentina', label: 'Argentina' },  { value: 'venezuela', label: 'Venezuela' },  { value: 'japan', label: 'Japan' },  { value: 'france', label: 'France' },  { value: 'italy', label: 'Italy' },  { value: 'spain', label: 'Spain' },]</script><template>  <div class="flex flex-col gap-4">    <Autocomplete      disabled      clearable      class="w-[256px]"      label="State"      placeholder="Select one"      search-label="Search states"      search-placeholder="Search states..."      default-value="california"      :items="items"    >      <template #empty>        No results found      </template>    </Autocomplete>    <Autocomplete      disabled      clearable      class="w-[256px]"      label="Countries to Visit"      placeholder="Select countries"      selection-mode="multiple"      :default-value="['argentina', 'japan', 'france']"      search-label="Search countries"      search-placeholder="Search countries..."      :items="countries"    >      <template #empty>        No results found      </template>    </Autocomplete>  </div></template>

With Disabled Options

Animal
Select an animal
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'dog', label: 'Dog' },  { value: 'cat', label: 'Cat', disabled: true },  { value: 'bird', label: 'Bird' },  { value: 'kangaroo', label: 'Kangaroo', disabled: true },  { value: 'elephant', label: 'Elephant' },  { value: 'tiger', label: 'Tiger' },]</script><template>  <Autocomplete    clearable    class="w-[256px]"    label="Animal"    placeholder="Select an animal"    search-label="Search animals"    search-placeholder="Search animals..."    :items="animals"  >    <template #empty>      No results found    </template>  </Autocomplete></template>

Allows Empty Collection

The allows-empty-collection prop enables the autocomplete to function even when there are no items in the collection. This is useful for scenarios where the list might be empty initially or when all items are filtered out.

State
Select one
<script setup lang="ts">import { Autocomplete } from '@hareui/vue'</script><template>  <!-- No options: with `allows-empty-collection` the popover still opens and shows the empty state -->  <Autocomplete    allows-empty-collection    class="w-[256px]"    label="State"    placeholder="Select one"    search-label="Search states"    search-placeholder="Search states..."    :items="[]"  >    <template #empty>      No results found    </template>  </Autocomplete></template>

With Sections

Country
Select a country
<script setup lang="ts">import type { ListBoxEntry } from '@hareui/vue'import { Autocomplete } from '@hareui/vue'const countries: ListBoxEntry[] = [  {    type: 'section',    label: 'North America',    items: [      { value: 'usa', label: 'United States' },      { value: 'canada', label: 'Canada' },      { value: 'mexico', label: 'Mexico' },    ],  },  { type: 'separator' },  {    type: 'section',    label: 'Europe',    items: [      { value: 'uk', label: 'United Kingdom' },      { value: 'france', label: 'France' },      { value: 'germany', label: 'Germany' },      { value: 'spain', label: 'Spain' },      { value: 'italy', label: 'Italy' },    ],  },  { type: 'separator' },  {    type: 'section',    label: 'Asia',    items: [      { value: 'japan', label: 'Japan' },      { value: 'china', label: 'China' },      { value: 'india', label: 'India' },      { value: 'south-korea', label: 'South Korea' },    ],  },]</script><template>  <Autocomplete    clearable    class="w-[256px]"    label="Country"    placeholder="Select a country"    search-label="Search countries"    search-placeholder="Search countries..."    :items="countries"  >    <template #empty>      No results found    </template>  </Autocomplete></template>

Multiple Select

In multiple mode the trigger lists the selected labels as text by default. For richer layouts such as removable tags, render your own content from selectedItems in the #value slot, as shown below.

States
Select states
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, TagGroup } from '@hareui/vue'import { ref } from 'vue'const items: ListBoxItem[] = [  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'florida', label: 'Florida' },  { value: 'new-york', label: 'New York' },  { value: 'illinois', label: 'Illinois' },  { value: 'pennsylvania', label: 'Pennsylvania' },]const selectedKeys = ref<string[]>([])function onRemoveTags(ids: string[]) {  selectedKeys.value = selectedKeys.value.filter(key => !ids.includes(key))}</script><template>  <Autocomplete    v-model="selectedKeys"    clearable    class="w-[256px]"    label="States"    placeholder="Select states"    selection-mode="multiple"    search-placeholder="Search..."    :items="items"  >    <template #value="{ selectedItems, isPlaceholder, text }">      <template v-if="isPlaceholder">        {{ text }}      </template>      <TagGroup v-else size="sm" removable aria-label="Selected" :items="selectedItems.map(item => ({ id: String(item.value), label: item.label }))" @remove="onRemoveTags" />    </template>    <template #empty>      No results found    </template>  </Autocomplete></template>

Controlled

State (controlled)
California

Selected: California

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete } from '@hareui/vue'import { computed, ref } from 'vue'const states: ListBoxItem[] = [  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'florida', label: 'Florida' },  { value: 'new-york', label: 'New York' },  { value: 'illinois', label: 'Illinois' },  { value: 'pennsylvania', label: 'Pennsylvania' },]const state = ref<string | null>('california')const selectedState = computed(() => states.find(s => s.value === state.value))</script><template>  <div class="space-y-2">    <Autocomplete      v-model="state"      clearable      class="w-[256px]"      label="State (controlled)"      placeholder="Select a state"      search-label="Search states"      search-placeholder="Search states..."      :items="states"    >      <template #empty>        No results found      </template>    </Autocomplete>    <p class="text-sm text-muted">      Selected: {{ selectedState?.label || 'None' }}    </p>  </div></template>

Controlled Multiple

States
California and Texas

Selected: california, texas

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete } from '@hareui/vue'import { ref } from 'vue'const items: ListBoxItem[] = [  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'florida', label: 'Florida' },  { value: 'new-york', label: 'New York' },  { value: 'illinois', label: 'Illinois' },  { value: 'pennsylvania', label: 'Pennsylvania' },]const selected = ref<string[]>(['california', 'texas'])</script><template>  <div class="space-y-2">    <Autocomplete      v-model="selected"      clearable      class="w-[256px]"      label="States"      placeholder="Select states"      selection-mode="multiple"      search-label="Search states"      search-placeholder="Search states..."      :items="items"    >      <template #empty>        No results found      </template>    </Autocomplete>    <p class="text-sm text-muted">      Selected: {{ selected.length > 0 ? selected.join(', ') : 'None' }}    </p>  </div></template>

Controlled Open State

State
Select one

Autocomplete is closed

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, Button } from '@hareui/vue'import { ref } from 'vue'const items: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]const isOpen = ref(false)</script><template>  <div class="space-y-4">    <Autocomplete      v-model:open="isOpen"      clearable      class="w-[256px]"      label="State"      placeholder="Select one"      search-label="Search states"      search-placeholder="Search states..."      :items="items"    >      <template #empty>        No results found      </template>    </Autocomplete>    <Button @click="isOpen = !isOpen">      {{ isOpen ? 'Close' : 'Open' }} Autocomplete    </Button>    <p class="text-sm text-muted">      Autocomplete is {{ isOpen ? 'open' : 'closed' }}    </p>  </div></template>

Asynchronous Filtering

Bind v-model:input-value to fetch on every search and set :filter="false" so the fetched items are shown as they are. The #search-trailing slot holds the spinner.

Search a Star Wars characters
Search...
<script setup lang="ts">import { Autocomplete, Spinner } from '@hareui/vue'import { onMounted, ref, watch } from 'vue'interface Character {  name: string}// HeroUI's `useAsyncList`: refetch whenever the search text changes, abort the previous requestconst filterText = ref('')const characters = ref<Character[]>([])const isLoading = ref(false)let controller: AbortController | undefinedasync function load(text: string) {  controller?.abort()  controller = new AbortController()  isLoading.value = true  try {    const res = await fetch(`https://swapi.py4e.com/api/people/?search=${text}`, { signal: controller.signal })    characters.value = (await res.json()).results    isLoading.value = false  }  catch (error) {    if ((error as Error).name !== 'AbortError')      isLoading.value = false  }}watch(filterText, load)// Client only: VitePress SSR-renders demosonMounted(() => load(''))</script><template>  <Autocomplete    v-model:input-value="filterText"    allows-empty-collection    clearable    class="w-[256px]"    label="Search a Star Wars characters"    placeholder="Search..."    search-label="Search characters"    search-placeholder="Search characters..."    :filter="false"    :items="characters.map(c => ({ value: c.name, label: c.name }))"    :ui="{ searchField: 'sticky top-0 z-10', listBox: 'max-h-[420px] overflow-y-auto', searchClearButton: isLoading ? 'pointer-events-none opacity-0' : undefined }"  >    <template #search-trailing>      <Spinner size="sm" class="absolute end-2 top-1/2 -translate-y-1/2" :class="{ 'pointer-events-none opacity-0': !isLoading }" />    </template>    <template #empty>      No results found    </template>  </Autocomplete></template>

Custom Indicator

State
Select one
<script setup lang="ts">import { Icon } from '@iconify/vue'import type { ListBoxItem } from '@hareui/vue'import { Autocomplete } from '@hareui/vue'import { ref } from 'vue'const items: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]const selectedKey = ref<string | null>(null)</script><template>  <Autocomplete    v-model="selectedKey"    clearable    class="w-[256px]"    label="State"    placeholder="Select one"    search-label="Search states"    search-placeholder="Search states..."    :items="items"  >    <template #indicator>      <Icon class="size-3" icon="gravity-ui:chevrons-expand-vertical" />    </template>    <template #empty>      No results found    </template>  </Autocomplete></template>

Custom Value

You can customize the displayed value with the #value slot:

Currency
$USDUS Dollar
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete } from '@hareui/vue'interface Currency extends ListBoxItem {  code: string  name: string  symbol: string}const currencies = [  { value: 'usd', code: 'USD', name: 'US Dollar', symbol: '$' },  { value: 'eur', code: 'EUR', name: 'Euro', symbol: '€' },  { value: 'gbp', code: 'GBP', name: 'British Pound', symbol: '£' },  { value: 'jpy', code: 'JPY', name: 'Japanese Yen', symbol: '¥' },  { value: 'chf', code: 'CHF', name: 'Swiss Franc', symbol: '₣' },]const items: Currency[] = currencies.map(c => ({ ...c, label: c.code, description: c.name, textValue: `${c.code} ${c.name}` }))</script><template>  <Autocomplete    class="w-[256px]"    default-value="usd"    label="Currency"    placeholder="Select a currency"    search-label="Search currencies"    search-placeholder="Search currencies..."    :items="items"  >    <template #value="{ selectedItems, isPlaceholder, text }">      <template v-if="isPlaceholder || !selectedItems[0]">        {{ text }}      </template>      <span v-else class="flex min-w-0 items-center gap-1.5">        <span class="font-medium">{{ selectedItems[0]!.symbol }}</span>        <span>{{ selectedItems[0]!.code }}</span>        <span class="truncate text-muted">{{ selectedItems[0]!.name }}</span>      </span>    </template>    <template #empty>      No results found    </template>  </Autocomplete></template>

In Surface

When used inside a Surface component, use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.

State
Select one
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, Surface } from '@hareui/vue'import { ref } from 'vue'const items: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]const selectedKey = ref<string | null>(null)</script><template>  <Surface class="w-[320px] rounded-3xl p-6">    <Autocomplete      v-model="selectedKey"      clearable      class="w-full"      label="State"      placeholder="Select one"      variant="secondary"      search-label="Search states"      search-placeholder="Search states..."      :items="items"    >      <template #empty>        No results found      </template>    </Autocomplete>  </Surface></template>

Virtualization

Set virtualize to render only the rows in view, for efficient rendering of large datasets. Pass { estimateSize } with the row height in pixels (default 50); sections and separators aren't rendered in this mode. Here the 1,000 users are filtered on name or email with :filter="false" and v-model:input-value.

User
Select a user
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete } from '@hareui/vue'import { useFilter } from 'reka-ui'import { computed, ref } from 'vue'interface User extends ListBoxItem {  label: string  description: string}function generateUsers(n: number): User[] {  const firstNames = ['Emma', 'Liam', 'Olivia', 'Noah', 'Ava', 'James', 'Sophia', 'Oliver', 'Isabella', 'Lucas', 'Mia', 'Ethan', 'Charlotte', 'Mason', 'Amelia', 'Logan', 'Harper', 'Alexander', 'Ella', 'Benjamin']  const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez', 'Anderson', 'Taylor', 'Thomas', 'Jackson', 'White', 'Harris', 'Clark', 'Lewis', 'Robinson', 'Walker']  return Array.from({ length: n }, (_, i) => {    const firstName = firstNames[i % firstNames.length]!    const lastName = lastNames[Math.floor(i / firstNames.length) % lastNames.length]!    return { value: i + 1, label: `${firstName} ${lastName}`, description: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@acme.com` }  })}const allUsers = generateUsers(1000)const selectedKey = ref<number | null>(null)const searchQuery = ref('')const { contains } = useFilter({ sensitivity: 'base' })// Filter on name or email ourselves; `:filter="false"` shows the items as givenconst filteredUsers = computed(() => searchQuery.value  ? allUsers.filter(user => contains(user.label, searchQuery.value) || contains(user.description, searchQuery.value))  : allUsers)</script><template>  <Autocomplete    v-model="selectedKey"    v-model:input-value="searchQuery"    allows-empty-collection    clearable    class="w-[300px]"    label="User"    placeholder="Select a user"    search-label="Search users"    search-placeholder="Search users..."    :filter="false"    :items="filteredUsers"    :virtualize="{ estimateSize: 50 }"    :ui="{ searchField: 'sticky top-0 z-10' }"  >    <template #empty>      No results found    </template>  </Autocomplete></template>

Advanced Examples

User Selection

User
Select a user
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, Avatar } from '@hareui/vue'import { ref } from 'vue'interface User extends ListBoxItem {  avatarUrl: string  fallback: string}const users: User[] = [  { value: '1', label: 'Bob', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg', fallback: 'B' },  { value: '2', label: 'Fred', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg', fallback: 'F' },  { value: '3', label: 'Martha', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg', fallback: 'M' },  { value: '4', label: 'John', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg', fallback: 'J' },  { value: '5', label: 'Jane', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg', fallback: 'J' },]const selectedKey = ref<string | null>(null)</script><template>  <Autocomplete    v-model="selectedKey"    clearable    class="w-[256px]"    label="User"    placeholder="Select a user"    :search="false"    :items="users"  >    <template #value="{ selectedItems, isPlaceholder, text }">      <template v-if="isPlaceholder || !selectedItems[0]">        {{ text }}      </template>      <div v-else class="flex items-center gap-2">        <Avatar class="size-4" size="sm" :src="selectedItems[0]!.avatarUrl" :fallback="selectedItems[0]!.fallback" />        <span>{{ selectedItems[0]!.label }}</span>      </div>    </template>    <template #item-leading="{ item }">      <Avatar size="sm" :src="item.avatarUrl" :fallback="item.fallback" />    </template>    <template #empty>      No results found    </template>  </Autocomplete></template>

User Selection Multiple

Users
BBob
FFred
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, Avatar, TagGroup } from '@hareui/vue'import { ref } from 'vue'interface User extends ListBoxItem {  avatarUrl: string  fallback: string}const users: User[] = [  { value: '1', label: 'Bob', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg', fallback: 'B' },  { value: '2', label: 'Fred', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg', fallback: 'F' },  { value: '3', label: 'Martha', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg', fallback: 'M' },  { value: '4', label: 'John', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg', fallback: 'J' },  { value: '5', label: 'Jane', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg', fallback: 'J' },]const selectedKeys = ref<string[]>(['1', '2'])function onRemoveTags(ids: string[]) {  selectedKeys.value = selectedKeys.value.filter(key => !ids.includes(key))}</script><template>  <Autocomplete    v-model="selectedKeys"    clearable    class="w-[256px]"    label="Users"    placeholder="Select your teammates"    selection-mode="multiple"    :search="false"    :items="users"  >    <template #value="{ selectedItems, isPlaceholder, text }">      <template v-if="isPlaceholder">        {{ text }}      </template>      <TagGroup v-else size="sm" removable aria-label="Selected users" :items="selectedItems.map(item => ({ id: String(item.value), label: item.label, avatarUrl: item.avatarUrl, fallback: item.fallback }))" @remove="onRemoveTags">        <template #item="{ item }">          <Avatar class="size-4" size="sm" :src="item.avatarUrl" :fallback="item.fallback" />          <span>{{ item.label }}</span>        </template>      </TagGroup>    </template>    <template #item-leading="{ item }">      <Avatar size="sm" :src="item.avatarUrl" :fallback="item.fallback" />    </template>    <template #empty>      No results found    </template>  </Autocomplete></template>
City
Search for a city
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete } from '@hareui/vue'import { ref, watch } from 'vue'const allCities = [  { country: 'USA', name: 'New York' },  { country: 'USA', name: 'Los Angeles' },  { country: 'USA', name: 'Chicago' },  { country: 'UK', name: 'London' },  { country: 'France', name: 'Paris' },  { country: 'Japan', name: 'Tokyo' },  { country: 'Australia', name: 'Sydney' },  { country: 'Canada', name: 'Toronto' },  { country: 'Germany', name: 'Berlin' },  { country: 'Spain', name: 'Madrid' },]const items: ListBoxItem[] = allCities.map(city => ({ value: city.name, label: city.name, description: city.country }))const selectedKey = ref<string | null>(null)const searchText = ref('')const isLoading = ref(false)// Simulate async filteringlet timer: ReturnType<typeof setTimeout> | undefinedwatch(searchText, (text) => {  if (!text)    return  isLoading.value = true  clearTimeout(timer)  timer = setTimeout(() => {    isLoading.value = false  }, 300)})</script><template>  <Autocomplete    v-model="selectedKey"    v-model:input-value="searchText"    clearable    class="w-[256px]"    label="City"    placeholder="Search for a city"    search-label="Search cities"    search-placeholder="Search cities..."    :items="items"  >    <template #empty>      {{ isLoading ? 'Searching...' : 'No cities found' }}    </template>  </Autocomplete></template>

Tag Group Selection

Set :search="false" to drop the search field; the list then takes focus as in Select.

Tags
Select tags
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, TagGroup } from '@hareui/vue'import { ref } from 'vue'const tags: ListBoxItem[] = [  { value: 'react', label: 'React' },  { value: 'typescript', label: 'TypeScript' },  { value: 'javascript', label: 'JavaScript' },  { value: 'nodejs', label: 'Node.js' },  { value: 'python', label: 'Python' },  { value: 'vue', label: 'Vue' },  { value: 'angular', label: 'Angular' },  { value: 'nextjs', label: 'Next.js' },]const selectedKeys = ref<string[]>([])function onRemoveTags(ids: string[]) {  selectedKeys.value = selectedKeys.value.filter(key => !ids.includes(key))}</script><template>  <Autocomplete    v-model="selectedKeys"    clearable    class="w-[256px]"    label="Tags"    placeholder="Select tags"    selection-mode="multiple"    :search="false"    :items="tags"  >    <template #value="{ selectedItems, isPlaceholder, text }">      <template v-if="isPlaceholder">        {{ text }}      </template>      <TagGroup v-else size="sm" removable aria-label="Selected" :items="selectedItems.map(item => ({ id: String(item.value), label: item.label }))" @remove="onRemoveTags" />    </template>    <template #empty>      No tags found    </template>  </Autocomplete></template>

Email Recipients

To
Add recipients
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, TagGroup } from '@hareui/vue'import { ref } from 'vue'const emails: ListBoxItem[] = [  { value: '[email protected]', label: 'Alice Johnson', description: '[email protected]', textValue: '[email protected]' },  { value: '[email protected]', label: 'Bob Smith', description: '[email protected]', textValue: '[email protected]' },  { value: '[email protected]', label: 'Charlie Brown', description: '[email protected]', textValue: '[email protected]' },  { value: '[email protected]', label: 'Diana Prince', description: '[email protected]', textValue: '[email protected]' },  { value: '[email protected]', label: 'Eve Wilson', description: '[email protected]', textValue: '[email protected]' },]const selectedKeys = ref<string[]>([])function onRemoveTags(ids: string[]) {  selectedKeys.value = selectedKeys.value.filter(key => !ids.includes(key))}</script><template>  <Autocomplete    v-model="selectedKeys"    clearable    class="w-[256px]"    label="To"    placeholder="Add recipients"    selection-mode="multiple"    search-label="Search emails"    search-placeholder="Search emails..."    :items="emails"  >    <template #value="{ selectedItems, isPlaceholder, text }">      <template v-if="isPlaceholder">        {{ text }}      </template>      <TagGroup v-else size="sm" removable aria-label="Selected" :items="selectedItems.map(item => ({ id: String(item.value), label: String(item.value) }))" @remove="onRemoveTags" />    </template>    <template #empty>      No recipients found    </template>  </Autocomplete></template>

Customization

Tailwind CSS

People who will be notified when this task updates.
Search teammates...
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Autocomplete, Description, Label, TagGroup } from '@hareui/vue'import { ref } from 'vue'const TEAMMATES = [  { id: 'sarah', name: 'Sarah Chen', role: 'Product Design' },  { id: 'marcus', name: 'Marcus Lee', role: 'Engineering' },  { id: 'priya', name: 'Priya Patel', role: 'Data Science' },  { id: 'jordan', name: 'Jordan Kim', role: 'Customer Success' },  { id: 'alex', name: 'Alex Rivera', role: 'Marketing' },] as constconst itemClass = 'rounded-lg data-[focused=true]:bg-accent/10 data-[selected=true]:bg-accent/5 dark:data-[focused=true]:bg-accent/15 dark:data-[selected=true]:bg-accent/8'interface Teammate extends ListBoxItem {  name: string  role: string}const items: Teammate[] = TEAMMATES.map(t => ({ value: t.id, label: t.name, textValue: `${t.name} ${t.role}`, class: itemClass, name: t.name, role: t.role }))const selectedKeys = ref<string[]>([])function onRemoveTags(ids: string[]) {  selectedKeys.value = selectedKeys.value.filter(key => !ids.includes(key))}</script><template>  <div class="w-full max-w-sm pt-6">    <!-- HeroUI places the Description between the Label and the trigger; here both sit above the component -->    <div class="flex w-full flex-col gap-1.5">      <Label id="assignees-label" class="font-medium text-foreground">        Assignees      </Label>      <Description id="assignees-description">        People who will be notified when this task updates.      </Description>      <Autocomplete        v-model="selectedKeys"        clearable        class="w-full"        aria-labelledby="assignees-label"        placeholder="Search teammates..."        selection-mode="multiple"        search-label="Search by name or role"        search-placeholder="Search by name or role..."        :items="items"        :ui="{          trigger: 'rounded-xl border border-border/80 bg-surface shadow-sm ring-1 ring-accent/5 transition-[box-shadow,border-color] focus-within:border-accent/25 focus-within:ring-2 focus-within:ring-accent/15 dark:border-border/90 dark:bg-surface dark:ring-accent/10 dark:focus-within:border-accent/35',          popover: 'overflow-hidden rounded-xl border border-border/80 bg-surface shadow-xl ring-1 ring-black/3 dark:border-border/90 dark:bg-surface dark:ring-white/5',          searchGroup: 'border-b border-separator bg-surface-secondary/50',          searchIcon: 'text-muted',          listBox: 'p-1.5',        }"      >        <template #value="{ selectedItems, isPlaceholder, text }">          <template v-if="isPlaceholder">            {{ text }}          </template>          <TagGroup            v-else            size="sm"            removable            aria-label="Assignees"            :items="selectedItems.map(item => ({ id: String(item.value), label: item.label }))"            :ui="{ tag: 'border border-accent/15 bg-accent/8 text-foreground dark:border-accent/25 dark:bg-accent/12' }"            @remove="onRemoveTags"          />        </template>        <template #item-label="{ item }">          <div class="flex flex-col gap-0.5">            <span class="text-sm font-medium text-foreground">{{ item.name }}</span>            <span class="text-xs text-muted">{{ item.role }}</span>          </div>        </template>        <template #empty>          No teammates found        </template>      </Autocomplete>    </div>  </div></template>

Global Configuration

app.use(createHareUI({
  ui: {
    autocomplete: {
      slots: {
        trigger: 'rounded-lg border border-border bg-surface p-2',
        value: 'text-current',
        clearButton: 'text-muted hover:text-foreground',
        indicator: 'text-muted',
        popover: 'rounded-lg border border-border bg-surface p-2',
      },
    },
  },
}))

Styling Reference

Interactive States

  • Hover: :hover or [data-hovered="true"] on the trigger
  • Focus: [data-focus-visible="true"] on the trigger
  • Disabled: [data-disabled="true"] on the autocomplete and the trigger
  • Invalid: [data-invalid="true"] on the autocomplete
  • Placeholder: [data-placeholder="true"] on the value
  • Empty: [data-empty="true"] on the clear button when nothing is selected
  • Open: [data-open="true"] on the autocomplete and the indicator

API Reference

Props

PropTypeDefaultDescription
itemsListBoxEntry[]-Options, sections ({ type: 'section', label, items }) and separators ({ type: 'separator' }), as in ListBox.
selectionMode"multiple" | "single""single"Whether one or several options can be selected. In 'multiple' mode v-model is an array.
defaultValueAutocompleteValueundefinedThe initial selection when uncontrolled (no v-model).
placeholderstring"Select an item"Text shown in the trigger while nothing is selected.
labelstring-Label text rendered above the trigger.
descriptionstring-Helper text rendered below the trigger. Hidden while invalid.
errorMessagestring-Error text rendered below the trigger while invalid. Defaults to the validation messages.
idstring-Id of the trigger button.
namestring-Name of the hidden native <select>, submitted with forms.
autocompletestring-autocomplete hint of the hidden native <select>.
openbooleanundefinedWhether the popover is open (v-model:open).
defaultOpenbooleanfalseWhether the popover starts open when uncontrolled.
searchbooleantrueWhether the popover has a search field that filters the options (HeroUI Autocomplete.Filter + SearchField). Without it, the list takes focus as in Select.
searchPlaceholderstring"Search..."Placeholder of the search field.
searchLabelstring"Search options"Accessible name of the search field.
defaultInputValuestring-The initial search text when uncontrolled (no v-model:input-value).
filterfalse | ((text: string, inputValue: string) => boolean)undefinedFilters options against the search text. Defaults to a case- and accent-insensitive "contains" match on each option's textValue ?? label. Pass false to show every item (e.g. when items are fetched per search).
allowsEmptyCollectionbooleanfalseWhether the popover opens when there are no options.
clearablebooleanfalseWhether to render a clear button in the trigger. Backspace / Delete on the trigger also clears.
loadingbooleanundefinedAsync options: shows the #loading slot after the last option and emits load-more at the end of the list.
virtualizeboolean | { estimateSize?: number | undefined; overscan?: number | undefined; }falseRender only the visible rows (Reka ListboxVirtualizer), for long lists. Sections and separators are skipped. Pass { estimateSize, overscan } to tune; rows are estimateSize (default 50) px apart.
variant"primary" | "secondary"'primary'Visual variant. Use secondary on surfaces.
fullWidthbooleanfalseWhether the autocomplete takes the full width of its container.
disabledbooleanfalseWhether the autocomplete is disabled.
requiredbooleanfalseWhether a selection is required before the form can be submitted.
invalidbooleanundefinedControlled invalid state; overrides validation.
validateValidateFn<AutocompleteValue>-Custom validation, called with the current selection. Return an error message (or several) when invalid.
validationBehaviorValidationBehavior'native' (or the enclosing Form's)native blocks form submission and shows errors on commit; aria shows errors in realtime without blocking.
ui(ComponentSlots<{ slots: { base: string; label: string; trigger: string[]; value: string[]; clearButton: string[]; indicator: string[]; popover: string[]; searchField: string; searchGroup: string; searchIcon: string; searchInput: string; listBox: string; }; variants: { variant: { primary: {}; secondary: { trigger: string; }; }; fullWidth: { true: { base: string; trigger: string; }; false: {}; }; }; defaultVariants: { variant: string; fullWidth: boolean; }; }> & { searchClearButton?: any; })-Per-slot class overrides.
modelValueAutocompleteValue--
inputValuestring--

Slots

SlotPropsDescription
labelanyLabel content. Replaces label.
valueAutocompleteValueSlotPropsTrigger value content. Defaults to the selected label(s) or the placeholder.
indicator{ open: boolean; }Trigger indicator content. Defaults to a chevron.
clearanyClear button content. Defaults to a close icon.
descriptionanyDescription content. Replaces description.
errorValidationResultError content, rendered while invalid.
search-trailing{ inputValue: string; }Content after the search input (e.g. a Spinner while fetching).
itemListBoxItemSlotPropsWhole content of each option (as ListBox #item).
item-leadingListBoxItemSlotPropsContent before each option's text (ListBox #item-leading).
item-labelListBoxItemSlotPropsEach option's label (ListBox #item-label).
item-descriptionListBoxItemSlotPropsEach option's description (ListBox #item-description).
item-trailingListBoxItemSlotPropsContent after each option's text (ListBox #item-trailing).
item-indicatorListBoxItemSlotPropsOption checkmark content (ListBox #indicator).
section-label{ section: ListBoxSection; }Section heading (ListBox #section-label).
emptyanyEmpty-state content, rendered when no option matches (HeroUI renderEmptyState).
loadinganyShown after the last option while loading is true.

Emits

EventPayloadDescription
clear[]-
update:open[value: boolean]-
update:inputValue[value: string]-
loadMore[]-
update:modelValue[value: AutocompleteValue | undefined]-
update:inputValue[value: string | undefined]-

Accessibility

Autocomplete follows the ARIA select-only pattern with a filter field:

  • The trigger button opens the popover with click, Enter, Space or the arrow keys; typing on the closed trigger selects the next matching option
  • Focus moves into the search field; the arrow keys, Home / End and Page Up / Page Down move a virtual focus through the options (aria-activedescendant), Enter selects
  • Escape clears the search text first, then closes the popover and returns focus to the trigger
  • The clear button is excluded from the tab order; with clearable, Backspace / Delete on the trigger also clears
  • A hidden native <select> carries name, form data and native required validation

On this page

No Headings