sulaf
DocsComponentsComposables
X
Sections
  • Get Started
  • Installation
  • Components
  • Composables
  • Animations
    Soon
Components
  • Autocomplete
    New
  • Show More
    New
  • Meter
    New
  • Contribution Heatmap
    New
  • Phone Input
    New
  • Typography
    New
  • Code Block
    Soon
  • Code Snippet
    Soon
  • Guided Tour
    Soon
  • Star Rating
    Soon
Composables
  • useGithubProfile
    Beta
  • useIsMac
    New
Animations
Soon

Autocomplete

PreviousNext

A searchable selection component with filtering capabilities, built on top of Reka UI Combobox.

Docs API Reference
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useFilter } from 'reka-ui'
import { ChevronDownIcon, SearchIcon } from 'lucide-vue-next'
import {
  Autocomplete,
  AutocompleteContent,
  AutocompleteControl,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompleteTrigger,
  AutocompleteClear,
  AutocompleteLabel,
  AutocompleteLoading,
  AutocompleteGroup,
} from '~/components/ui/autocomplete'

const items = ['Vue', 'React', 'Svelte', 'Angular', 'Solid', 'Nuxt', 'Next.js']

const searchTerm = ref('')
const value = ref<string>()
const isLoading = ref(false)

const { contains } = useFilter({ sensitivity: 'base' })

const filteredItems = computed(() => {
  if (!searchTerm.value) return items
  return items.filter(item => contains(item, searchTerm.value))
})

let timer: ReturnType<typeof setTimeout>
watch(searchTerm, newVal => {
  if (newVal) {
    isLoading.value = true
    clearTimeout(timer)
    timer = setTimeout(() => {
      isLoading.value = false
    }, 250)
  } else {
    isLoading.value = false
  }
})
</script>

<template>
  <Autocomplete v-model="value" v-model:search-term="searchTerm">
    <AutocompleteControl>
      <SearchIcon class="size-4 ms-2" />
      <AutocompleteInput placeholder="Search..." />

      <AutocompleteClear />

      <AutocompleteTrigger as-child>
        <div class="flex items-center pl-2">
          <ChevronDownIcon class="size-4 me-2" />
        </div>
      </AutocompleteTrigger>
    </AutocompleteControl>

    <AutocompleteContent>
      <AutocompleteLoading v-if="isLoading" />

      <template v-else>
        <AutocompleteEmpty>No framework found.</AutocompleteEmpty>

        <AutocompleteList>
          <AutocompleteGroup>
            <AutocompleteLabel>Framework</AutocompleteLabel>
            <AutocompleteItem v-for="item in filteredItems" :key="item" :value="item">
              {{ item }}
            </AutocompleteItem>
          </AutocompleteGroup>
        </AutocompleteList>
      </template>
    </AutocompleteContent>
  </Autocomplete>
</template>

The Autocomplete allows you to create a searchable selection component with filtering capabilities. It is built on top of Reka UI's Combobox primitive.

Installation

pnpm dlx sulaf@latest add autocomplete

Usage

<script setup lang="ts">
import {
  Autocomplete,
  AutocompleteContent,
  AutocompleteControl,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
} from '@/components/ui/autocomplete'
</script>

<template>
  <Autocomplete>
    <AutocompleteControl>
      <AutocompleteInput placeholder="Search..." />
    </AutocompleteControl>
    <AutocompleteContent>
      <AutocompleteEmpty>No results found.</AutocompleteEmpty>
      <AutocompleteList>
        <AutocompleteItem value="item-1">Item 1</AutocompleteItem>
        <AutocompleteItem value="item-2">Item 2</AutocompleteItem>
      </AutocompleteList>
    </AutocompleteContent>
  </Autocomplete>
</template>

Examples

Multi-select

Bind to an array to allow multiple selections. Use the multiple prop on the root component.

<script setup lang="ts">
import { ref, computed } from 'vue'
import { useFilter } from 'reka-ui'
import { ChevronDownIcon, SearchIcon, XIcon } from 'lucide-vue-next'
import {
  Autocomplete,
  AutocompleteContent,
  AutocompleteControl,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompleteTrigger,
  AutocompleteClear,
  AutocompleteLabel,
  AutocompleteGroup,
} from '~/components/ui/autocomplete'
import { Badge } from '~/components/ui/badge'

const items = ['Vue', 'React', 'Svelte', 'Angular', 'Solid', 'Nuxt', 'Next.js']

const searchTerm = ref('')
const value = ref<string[]>([])

const { contains } = useFilter({ sensitivity: 'base' })

const filteredItems = computed(() => {
  if (!searchTerm.value) return items
  return items.filter(item => contains(item, searchTerm.value))
})

function removeItem(item: string) {
  value.value = value.value.filter(v => v !== item)
}
</script>

<template>
  <div class="w-full max-w-md space-y-4">
    <div v-if="value.length > 0" class="flex flex-wrap gap-2">
      <Badge v-for="v in value" :key="v" variant="secondary" class="gap-1">
        {{ v }}
        <button
          type="button"
          class="outline-none"
          :aria-label="`Remove ${v}`"
          @click="removeItem(v)"
        >
          <XIcon class="size-3" />
        </button>
      </Badge>
    </div>

    <Autocomplete v-model="value" v-model:search-term="searchTerm" multiple>
      <AutocompleteControl>
        <SearchIcon class="size-4 ms-2" />
        <AutocompleteInput placeholder="Select frameworks..." />

        <AutocompleteClear />

        <AutocompleteTrigger as-child>
          <div class="flex items-center pl-2">
            <ChevronDownIcon class="size-4 me-2" />
          </div>
        </AutocompleteTrigger>
      </AutocompleteControl>

      <AutocompleteContent>
        <AutocompleteEmpty>No framework found.</AutocompleteEmpty>

        <AutocompleteList>
          <AutocompleteGroup>
            <AutocompleteLabel>Frameworks</AutocompleteLabel>
            <AutocompleteItem v-for="item in filteredItems" :key="item" :value="item">
              {{ item }}
            </AutocompleteItem>
          </AutocompleteGroup>
        </AutocompleteList>
      </AutocompleteContent>
    </Autocomplete>
  </div>
</template>

Async Search

Handle search terms manually to fetch data from an external API.

<script setup lang="ts">
import { ref, watch } from 'vue'
import { SearchIcon, Loader2Icon } from 'lucide-vue-next'
import {
  Autocomplete,
  AutocompleteContent,
  AutocompleteControl,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompleteLoading,
} from '~/components/ui/autocomplete'

interface User {
  id: number
  firstName: string
  username: string
}

const searchTerm = ref('')
const value = ref<User>()
const items = ref<User[]>([])
const isLoading = ref(false)
const isOpen = ref(false)

// Store the current AbortController instance
let abortController: AbortController | null = null

async function fetchUsers(query: string) {
  // Cancel any ongoing request
  if (abortController) {
    abortController.abort()
  }

  if (!query) {
    items.value = []
    isLoading.value = false
    return
  }

  // Create a new AbortController for this request
  const controller = new AbortController()
  abortController = controller

  isLoading.value = true
  try {
    const response = await fetch(`https://dummyjson.com/users`, {
      signal: controller.signal,
    })
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    const data: User[] = (await response.json())?.users ?? []
    items.value = data.filter(
      user =>
        user.firstName.toLowerCase().includes(query.toLowerCase()) ||
        user.username.toLowerCase().includes(query.toLowerCase()),
    )
  } catch (error) {
    // Ignore abort errors as they're intentional
    if (error instanceof Error && error.name === 'AbortError') {
      return
    }
    // eslint-disable-next-line no-console
    console.error('[Demo]: Failed to fetch users:', error)
  } finally {
    // Only clear state if we're still the active request
    if (abortController === controller) {
      isLoading.value = false
      abortController = null
    }
  }
}

watch(searchTerm, newVal => {
  fetchUsers(newVal)
})
</script>

<template>
  <div class="w-full max-w-md">
    <Autocomplete v-model="value" v-model:open="isOpen" v-model:search-term="searchTerm">
      <AutocompleteControl>
        <SearchIcon v-if="!isLoading" class="size-4 ms-2" />
        <Loader2Icon v-else class="size-4 ms-2 animate-spin" />

        <AutocompleteInput placeholder="Search users by name..." />
      </AutocompleteControl>

      <AutocompleteContent>
        <AutocompleteLoading v-if="isLoading"> Searching for users... </AutocompleteLoading>

        <template v-else>
          <AutocompleteEmpty v-if="searchTerm && items.length === 0">
            No users found for "{{ searchTerm }}".
          </AutocompleteEmpty>

          <AutocompleteList v-if="items.length > 0">
            <AutocompleteItem v-for="user in items" :key="user.id" :value="user">
              <div class="flex flex-col">
                <span>{{ user.firstName }}</span>
                <span class="text-xs text-muted-foreground">@{{ user.username }}</span>
              </div>
            </AutocompleteItem>
          </AutocompleteList>
        </template>
      </AutocompleteContent>
    </Autocomplete>

    <div v-if="value" class="mt-4 text-sm text-muted-foreground">
      Selected:
      <span class="font-medium text-foreground">{{ value.firstName }}</span>
    </div>
  </div>
</template>

API Reference

The Autocomplete component uses Reka UI's Combobox primitive under the hood, but introduces a few custom abstractions, models, and styling defaults.

Autocomplete (Root)

The root component extends all ComboboxRoot properties and adds semantic handling for the search input state.

PropTypeDefaultDescription
v-model:search-termstring''Exposes the direct value of the AutocompleteInput. By binding to this, you can filter your own item lists dynamically outside the component.

Accessibility

The Autocomplete component follows the WAI-ARIA Combobox pattern.

Keyboard Interactions

KeyAction
ArrowDownNavigates to the next item in the list.
ArrowUpNavigates to the previous item in the list.
EnterSelects the currently focused item.
EscapeCloses the list panel.
Home / EndMoves focus to the first or last item in the list.

ARIA Attributes

  • The input uses aria-autocomplete="list", aria-expanded, and aria-controls to manage the relationship with the dropdown.
  • AutocompleteLoading includes aria-live="polite" to announce loading states to screen readers.
  • AutocompleteClear and AutocompleteTrigger include default aria-label attributes for semantic clarity.

Labeling

Always provide a label for the input for better accessibility. You can use an external <label> or pass aria-label to the AutocompleteInput.

<AutocompleteInput aria-label="Select a framework" />
ComponentsShow More

On This Page

InstallationUsageExamplesMulti-selectAsync SearchAPI ReferenceAutocomplete (Root)AccessibilityKeyboard InteractionsARIA AttributesLabeling
© 2026 - Built with shadcn-vue . The source code is available on GitHub.