Skip to content

CAutocomplete

CAutocomplete is a selection field with text search built on top of CTextField, CMenu and CList. Unlike CSelect, the field stays editable: typing filters the options by the beginning of their title. It supports single and multiple selection, plain or object items (with title-key / value-key), chips and clearing. Any CTextField prop (label, placeholder, clearable, rules, disabled…) is forwarded to the underlying field.

Basic usage

Pass an array of values to items and bind the selection with v-model. Focus opens the menu, typing filters the list; picking an option stores the value and clears the search string.

Shipping destination
Pick a city to check courier coverage
Show code
vue
<template>
  <c-autocomplete
    v-model="destination"
    label="City"
    placeholder="Start typing — try «B»"
    :items="cities"
    clearable
  />

  <div v-if="destination" class="d-flex items-center gap-2 fs-sm mt-4">
    <c-icon name="fas:check" source="fa" :size="12" class="text-green" />
    Courier available in <b>{{ destination }}</b>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const destination = ref<string>()
const cities = ['Amsterdam', 'Barcelona', 'Berlin', 'Lisbon', 'London', 'Paris']
</script>

Object items

When items are objects, use title-key to pick the displayed label and value-key to pick the stored value. Both accept a dotted path (e.g. user.name). Search matches against the title.

Review request
Search matches the title (title-key), the model stores the id (value-key).
Show code
vue
<template>
  <c-autocomplete
    v-model="assignee"
    label="Reviewer"
    placeholder="Search a member"
    :items="members"
    title-key="name"
    value-key="id"
    clearable
  />

  <!-- v-model holds the id, the found object drives the summary card -->
  <div v-if="reviewer" class="d-flex items-center gap-2 mt-4">
    <b>{{ reviewer.name }}</b>
    <span class="fs-xs text-blue-grey">{{ reviewer.role }}</span>
    <c-chip>v-model: {{ assignee }}</c-chip>
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'

const assignee = ref<number>()
const members = [
  { id: 1, name: 'Anna Smith', role: 'Product Designer' },
  { id: 2, name: 'Boris Lee', role: 'Software Engineer' },
  { id: 3, name: 'Clara Diaz', role: 'Product Manager' },
]

const reviewer = computed(() => members.find((member) => member.id === assignee.value))
</script>

Omit value-key to store the whole item object instead of a single field. When titles are not unique, always provide value-key so distinct entries stay distinguishable.

Multiple selection

Add multiple to collect an array of values, and chips to render each selection as a removable chip. After a pick the menu stays open and focus returns to the field, so you can keep searching. Backspace in the empty field removes the last selected value.

Candidate stack 2 / 5
Vue
TypeScript
Pick 5 technologies. Backspace in the empty field removes the last one.
Show code
vue
<template>
  <c-autocomplete
    v-model="stack"
    label="Technologies"
    placeholder="Add a technology"
    :items="technologies"
    multiple
    chips
    clearable
  />

  <c-progress-linear
    :value="(stack.length / 5) * 100"
    :color="stack.length >= 5 ? 'green' : 'indigo'"
    height="6"
    class="mt-4"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'

const stack = ref(['Vue', 'TypeScript'])
const technologies = ['Vue', 'React', 'TypeScript', 'Node.js', 'Nuxt', 'Vite', 'Go', 'Rust']
</script>

Custom menu

The menu slot replaces the whole dropdown while keeping the component's filtering and selection: it receives the already-filtered items (NormalizedItem<T>[]) and onSelect. Put a CList inside the slot and keyboard works out of the box: the list registers itself into the autocomplete's keyboard loop — arrows from the field, Enter / Space and the after-pick search reset all work with zero wiring. Combined with the chips slot you can build a full people-picker — avatars, statuses, role badges and your own empty state:

Invite teammates 0 of 5 invited
Show code
vue
<template>
  <c-autocomplete
    v-model="invited"
    label="Teammates"
    placeholder="Search by name"
    title-key="name"
    :items="members"
    multiple
    chips
    clearable
  >
    <template #chips>
      <div v-for="member of invited" :key="member.email" class="invite-chip">
        <span class="avatar" :class="member.color">{{ initials(member) }}</span>
        {{ member.name.split(' ')[0] }}
        <c-icon name="fas:times" source="fa" :size="10" @click.stop="uninvite(member)" />
      </div>
    </template>

    <!-- Custom dropdown: own layout on top of items + onSelect -->
    <template #menu="{ items }">
      <div class="invite-menu radius-12 elevation-4">
        <div class="invite-menu__head">Team directory — {{ items.length }} matches</div>

        <div v-if="!items.length" class="pa-4 fs-sm text-blue-grey">Nobody matches this search</div>

        <c-list v-else v-model="invited" multiple variant="menu">
          <c-list-item v-for="item of items" :key="item.key" :value="item.raw">
            <span class="avatar" :class="item.raw.color">
              {{ initials(item.raw) }}
              <i class="dot" :class="item.raw.online ? 'bg-green' : 'bg-grey'"></i>
            </span>
            <c-list-item-content>
              <c-list-item-title>{{ item.title }}</c-list-item-title>
              <c-list-item-subtitle>{{ item.raw.email }}</c-list-item-subtitle>
            </c-list-item-content>
            <c-chip>{{ item.raw.role }}</c-chip>
            <c-icon
              v-if="invited.includes(item.raw)"
              name="fas:check"
              source="fa"
              :size="14"
              class="text-teal"
            />
          </c-list-item>
        </c-list>
      </div>
    </template>
  </c-autocomplete>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const invited = ref<Member[]>([])

const members: Member[] = [
  { name: 'Anna Smith', email: 'anna@vueland.dev', role: 'Design', color: 'bg-pink', online: true },
  {
    name: 'Boris Lee',
    email: 'boris@vueland.dev',
    role: 'Frontend',
    color: 'bg-indigo',
    online: true,
  },
  // ...
]

const initials = (member: Member) =>
  member.name
    .split(' ')
    .map((part) => part[0])
    .join('')

const uninvite = (member: Member) => {
  invited.value = invited.value.filter((it) => it !== member)
}
</script>

Selection state stays on the component: onSelect toggles the value in multiple mode, the search string keeps filtering items, and Backspace still removes the last pick.

  • Options are filtered case-insensitively by the beginning of the item title.
  • Every change of the search string emits update:search — use it for external logic such as highlighting, analytics or data loading.
  • Closing the menu in any way (pick, Escape, Tab, outside click) resets the search string, so the next opening shows the full list.

Behaviour

  • Focus opens the menu; Escape / Tab close it and blur the field.
  • ArrowUp / ArrowDown move the active option, Enter / Space select it.
  • In single mode a pick closes the menu and clears the search string; picking the already-selected option keeps it.
  • In multiple mode picking a selected option removes it. Pass mandatory to keep the last item.
  • Backspace in the empty field removes the last value (multiple) or clears the model (single).
  • readonly keeps the menu closed and Backspace does not remove selected values.

API

CAutocomplete props

Prop
Type
Default
Description
modelValue
T | T[]
Selected value(s); array in multiple mode
items
readonly T[]
[]
Available options
title-key
string
Path to the option's display label; search matches against it
value-key
string
Path to the value stored in v-model (defaults to the whole item)
multiple
boolean
false
Collect selected values into an array
mandatory
boolean
false
In multiple: keep the last item
chips
boolean
false
Render selected values as removable chips
options
{ noItemsMessage?: string }
Empty-list message

Any CTextField prop — label, placeholder, clearable, rules, disabled, readonly, … — is forwarded to the underlying field. Functions in rules receive the model value, not the search string.

Presets compose by value: the menu and list fields of an input preset take plain CMenuPreset and CListPreset objects — the same format standalone components use. CMenu, CList and CListItem pick them up from the same set via context.

ts
import { menuRounded } from './presets/menu' // CMenuPreset: root zone, opened/closed states
import { listCompact } from './presets/list' // CListPreset: root/option zones, disabled/readonly states

const combo: CInputPreset = {
  base: {
    field: ['text-indigo'],
    menu: menuRounded,
    list: listCompact,
  },
}

CAutocomplete events

Event
Arguments
Description
update:modelValue
T | T[]
Emitted when the selection changes
update:search
string
Emitted when the search string changes

CAutocomplete slots

Slot
Props
Description
chips
{ items: unknown[] }
Override how the selected values are displayed
menu
{ items: NormalizedItem<T>[], onSelect } & KeyboardAPI
Override the dropdown content; items is the already-filtered list
details
{ errorMessage?: string, details?: string }
Override the hint / error line
prepend
Content before the field
append
Content after the field; replaces the dropdown icon
no-items-message
Message shown when the search has no matches

In menu the onSelect function updates the model: in multiple mode calling it with an already-selected value removes it (toggle). The slot also receives the autocomplete's keyboard api: register / unregister to plug a custom target into the keyboard loop, forward to relay an event to the active target, and blur to reset its focus state. A CList-based menu needs none of it — the list registers itself.

NormalizedItem

The menu slot receives prepared entries:

ts
type NormalizedItem<T> = {
  raw: T
  title: unknown
  value: unknown
  key: string
}

raw is the original item, title is resolved via title-key, value via value-key, and key is used for stable list rendering.