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><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 key | data-slot | Element |
|---|---|---|
base | autocomplete | root <div> |
label | label | Label |
trigger | autocomplete-trigger | <div role="group"> holding the value, clear button and indicator button |
value | autocomplete-value | <span> inside the trigger |
clearButton | autocomplete-clear-button | <button> (with clearable) |
indicator | autocomplete-indicator / autocomplete-default-indicator | chevron |
popover | autocomplete-popover | popover |
searchField, searchGroup, searchIcon, searchInput, searchClearButton | search-field* | the search field (with search) |
listBox | list-box | the 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 stringExamples
Variants
The Autocomplete component supports two visual variants:
primary(default) - Standard styling with shadow, suitable for most use casessecondary- Lower emphasis variant without shadow, suitable for use in Surface components
Single Select Variants
Multiple Select Variants
<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
<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
<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
<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
<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
<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.
<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
<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.
<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
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
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
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.
<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
<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:
<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.
<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.
<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
<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
<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>Location Search
<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.
<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
<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
<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:
:hoveror[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
| Prop | Type | Default | Description |
|---|---|---|---|
items | ListBoxEntry[] | - | 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. |
defaultValue | AutocompleteValue | undefined | The initial selection when uncontrolled (no v-model). |
placeholder | string | "Select an item" | Text shown in the trigger while nothing is selected. |
label | string | - | Label text rendered above the trigger. |
description | string | - | Helper text rendered below the trigger. Hidden while invalid. |
errorMessage | string | - | Error text rendered below the trigger while invalid. Defaults to the validation messages. |
id | string | - | Id of the trigger button. |
name | string | - | Name of the hidden native <select>, submitted with forms. |
autocomplete | string | - | autocomplete hint of the hidden native <select>. |
open | boolean | undefined | Whether the popover is open (v-model:open). |
defaultOpen | boolean | false | Whether the popover starts open when uncontrolled. |
search | boolean | true | Whether the popover has a search field that filters the options (HeroUI Autocomplete.Filter + SearchField).
Without it, the list takes focus as in Select. |
searchPlaceholder | string | "Search..." | Placeholder of the search field. |
searchLabel | string | "Search options" | Accessible name of the search field. |
defaultInputValue | string | - | The initial search text when uncontrolled (no v-model:input-value). |
filter | false | ((text: string, inputValue: string) => boolean) | undefined | Filters 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). |
allowsEmptyCollection | boolean | false | Whether the popover opens when there are no options. |
clearable | boolean | false | Whether to render a clear button in the trigger. Backspace / Delete on the trigger also clears. |
loading | boolean | undefined | Async options: shows the #loading slot after the last option and emits load-more at the end of the list. |
virtualize | boolean | { estimateSize?: number | undefined; overscan?: number | undefined; } | false | Render 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. |
fullWidth | boolean | false | Whether the autocomplete takes the full width of its container. |
disabled | boolean | false | Whether the autocomplete is disabled. |
required | boolean | false | Whether a selection is required before the form can be submitted. |
invalid | boolean | undefined | Controlled invalid state; overrides validation. |
validate | ValidateFn<AutocompleteValue> | - | Custom validation, called with the current selection. Return an error message (or several) when invalid. |
validationBehavior | ValidationBehavior | '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. |
modelValue | AutocompleteValue | - | - |
inputValue | string | - | - |
Slots
| Slot | Props | Description |
|---|---|---|
label | any | Label content. Replaces label. |
value | AutocompleteValueSlotProps | Trigger value content. Defaults to the selected label(s) or the placeholder. |
indicator | { open: boolean; } | Trigger indicator content. Defaults to a chevron. |
clear | any | Clear button content. Defaults to a close icon. |
description | any | Description content. Replaces description. |
error | ValidationResult | Error content, rendered while invalid. |
search-trailing | { inputValue: string; } | Content after the search input (e.g. a Spinner while fetching). |
item | ListBoxItemSlotProps | Whole content of each option (as ListBox #item). |
item-leading | ListBoxItemSlotProps | Content before each option's text (ListBox #item-leading). |
item-label | ListBoxItemSlotProps | Each option's label (ListBox #item-label). |
item-description | ListBoxItemSlotProps | Each option's description (ListBox #item-description). |
item-trailing | ListBoxItemSlotProps | Content after each option's text (ListBox #item-trailing). |
item-indicator | ListBoxItemSlotProps | Option checkmark content (ListBox #indicator). |
section-label | { section: ListBoxSection; } | Section heading (ListBox #section-label). |
empty | any | Empty-state content, rendered when no option matches (HeroUI renderEmptyState). |
loading | any | Shown after the last option while loading is true. |
Emits
| Event | Payload | Description |
|---|---|---|
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>carriesname, form data and nativerequiredvalidation





