Keyboard shortcut: Ctrl + K
Get started

Combobox

A field that searches a list and keeps what is chosen, one value or several. The options may be flat, grouped or separated, and they may arrive from a server as the reader types.

Usage

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox } from 'vectis-ui'

const country = ref('')

const options = [
  { value: 'be', label: 'Belgium' },
  { value: 'ca', label: 'Canada' },
  { value: 'fr', label: 'France' },
  { value: 'ch', label: 'Switzerland' },
]
</script>

<template>
  <VCombobox
    v-model="country"
    :options="options"
    aria-label="Country"
    placeholder="Choose a country"
  />
</template>

Examples

Label and hint

label renders a descriptive text above the field, and hint renders a helper text below it.

vue

Type to narrow the list down. Accents are ignored, so reunion finds Réunion.

<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox } from 'vectis-ui'

const country = ref('fr')

const countries = [
  { value: 'be', label: 'Belgium' },
  { value: 'ca', label: 'Canada' },
  { value: 'ci', label: "Côte d'Ivoire" },
  { value: 'fr', label: 'France' },
  { value: 'lu', label: 'Luxembourg' },
  { value: 're', label: 'Réunion' },
  { value: 'ch', label: 'Switzerland' },
]
</script>

<template>
  <div class="column">
    <VCombobox
      v-model="country"
      :options="countries"
      label="Country"
      hint="Type to narrow the list down. Accents are ignored, so reunion finds Réunion."
      placeholder="Choose a country"
      clearable
    />
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
</style>

Sizes

Sets the field height to 32, 40, or 48 pixels. The compact prop reduces this height by 4px.

vue
France
France
France
France
France
France
<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox } from 'vectis-ui'

const countries = [
  { value: 'be', label: 'Belgium' },
  { value: 'ca', label: 'Canada' },
  { value: 'fr', label: 'France' },
  { value: 'ch', label: 'Switzerland' },
]

/* Multiple mode on every row: the chips are what shows that the panel and the field are
   not the only things following the step. */
const rows = ref(
  (['sm', 'md', 'lg'] as const).flatMap((size) => [
    { key: size, size, compact: false, label: size, selected: ['fr'] },
    { key: `${size}-compact`, size, compact: true, label: `${size}, compact`, selected: ['fr'] },
  ]),
)
</script>

<template>
  <div class="column">
    <VCombobox
      v-for="row in rows"
      :key="row.key"
      v-model="row.selected"
      :options="countries"
      :size="row.size"
      :compact="row.compact"
      :label="row.label"
      multiple
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

States

disabled makes the field unusable. readonly prevents changes but keeps the field focusable. invalid marks the field as having an error. loading displays a loading indicator. emptyText defines the message shown when there are no options. clearable adds an icon to clear the selection.

vue

The cross empties the selection and the search at once

For a rule the browser cannot check by itself

Set by your subscription

<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox } from 'vectis-ui'

const chosen = ref('fr')

const countries = [
  { value: 'be', label: 'Belgium' },
  { value: 'ca', label: 'Canada' },
  { value: 'fr', label: 'France' },
  { value: 'ch', label: 'Switzerland' },
]
</script>

<template>
  <div class="column">
    <VCombobox
      v-model="chosen"
      :options="countries"
      clearable
      label="Clearable"
      hint="The cross empties the selection and the search at once"
    />

    <VCombobox
      :options="countries"
      model-value="fr"
      invalid
      label="Invalid"
      hint="For a rule the browser cannot check by itself"
    />

    <VCombobox :options="countries" model-value="fr" disabled label="Disabled" />

    <!-- Frozen rather than out of reach: it still takes the focus and can be copied from. -->
    <VCombobox
      :options="countries"
      model-value="fr"
      readonly
      clearable
      label="Read-only"
      hint="Set by your subscription"
    />

    <VCombobox :options="[]" loading label="Loading" placeholder="Fetching the list" />

    <!-- No option and nothing loading: the panel says so rather than opening empty. -->
    <VCombobox
      :options="[]"
      empty-text="No country matches"
      label="Nothing to show"
      placeholder="Open me"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Placement

Sets the preferred opening direction (above or below the field) for the options list panel.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox } from 'vectis-ui'

/* Labels wider than the field: the panel is at least as wide as what it is anchored to,
   so it takes more than that here and the two alignments become visible. */
const departments = [
  { value: 'ops', label: 'Operations and logistics' },
  { value: 'fin', label: 'Finance and accounting' },
  { value: 'eng', label: 'Engineering and platform' },
  { value: 'hr', label: 'People and culture' },
]

const placements = ['bottom-start', 'bottom-end', 'top-start', 'top-end'] as const

const chosen = ref<Record<string, string>>({
  'bottom-start': 'ops',
  'bottom-end': 'fin',
  'top-start': 'eng',
  'top-end': 'hr',
})
</script>

<template>
  <div class="grid">
    <VCombobox
      v-for="placement in placements"
      :key="placement"
      v-model="chosen[placement]"
      :options="departments"
      :placement="placement"
      :label="placement"
    />
  </div>
</template>

<style scoped>
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Groups and separators

The options prop accepts a flat list, or can be structured with named groups and separators.

vue
Europe
Africa
America
<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox, type ComboboxItem } from 'vectis-ui'

const country = ref('fr')

/* An entry is an option, a named block, or a separator, and the three mix freely. A group
   the search empties disappears with its name, and a stranded separator is dropped. */
const items: ComboboxItem[] = [
  {
    label: 'Europe',
    options: [
      { value: 'fr', label: 'France' },
      { value: 'be', label: 'Belgium' },
      { value: 'ch', label: 'Switzerland' },
      { value: 'lu', label: 'Luxembourg' },
    ],
  },
  { separator: true },
  {
    label: 'Africa',
    options: [
      { value: 're', label: 'Réunion' },
      { value: 'ci', label: "Côte d'Ivoire" },
      { value: 'ma', label: 'Morocco' },
    ],
  },
  {
    label: 'America',
    options: [
      { value: 'ca', label: 'Canada' },
      { value: 'us', label: 'United States' },
      { value: 'br', label: 'Brazil' },
    ],
  },
  { separator: true },
  { value: 'other', label: 'Other, not listed' },
]
</script>

<template>
  <div class="column">
    <VCombobox v-model="country" :options="items" label="Country" placeholder="Choose a country" />
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
</style>

Multiple selection

multiple allows selecting several values, which are displayed as removable chips inside the field.

vue
FranceBelgium

Click elsewhere: the search field folds away and only the chips remain

fr, be
<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox } from 'vectis-ui'

const served = ref(['fr', 'be'])

const countries = [
  { value: 'be', label: 'Belgium' },
  { value: 'ca', label: 'Canada' },
  { value: 'ci', label: "Côte d'Ivoire" },
  { value: 'fr', label: 'France' },
  { value: 'lu', label: 'Luxembourg' },
  { value: 'mc', label: 'Monaco', disabled: true },
  { value: 're', label: 'Réunion' },
  { value: 'ch', label: 'Switzerland' },
]
</script>

<template>
  <div class="column">
    <VCombobox
      v-model="served"
      :options="countries"
      multiple
      clearable
      label="Served countries"
      hint="Click elsewhere: the search field folds away and only the chips remain"
      placeholder="Add a country"
    />
    <output class="value" aria-label="Chosen values">{{ served.join(', ') || 'none' }}</output>
  </div>
</template>

<style scoped>
.column {
  display: grid;
  gap: var(--vectis-space-3);
  max-inline-size: 26rem;
}
.value {
  font-family: var(--vectis-text-family-code);
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-text-muted);
}
</style>

Values as text

display="text" shows the chosen values as their labels joined by commas, on one line cut short with an ellipsis. The field keeps the height of an ordinary control, and under the focus the line leaves at least half of it to the search. A value is removed by unticking it in the list, with Backspace on an empty search, or with the clearable cross.

vue
France, Belgium, Switzerland, Luxembourg

One line of labels, cut short when it runs out of room

<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox } from 'vectis-ui'

const served = ref(['fr', 'be', 'ch', 'lu'])

const countries = [
  { value: 'be', label: 'Belgium' },
  { value: 'ca', label: 'Canada' },
  { value: 'ci', label: "Côte d'Ivoire" },
  { value: 'fr', label: 'France' },
  { value: 'lu', label: 'Luxembourg' },
  { value: 'mc', label: 'Monaco', disabled: true },
  { value: 're', label: 'Réunion' },
  { value: 'ch', label: 'Switzerland' },
]
</script>

<template>
  <div class="column">
    <VCombobox
      v-model="served"
      :options="countries"
      multiple
      display="text"
      clearable
      label="Served countries"
      hint="One line of labels, cut short when it runs out of room"
      placeholder="Add a country"
    />
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 20rem;
}
</style>

Values shown while folded

max keeps the first chosen values in view and sums the rest up as "+X", in chips or in text alike. It applies while the field is out of focus: focused, every value comes back so it can be seen and removed. overflowText rephrases the count, and the #overflow slot replaces it.

vue
FranceBelgium+3

Two chips, the rest counted until the field is focused

France, Belgium+3 countries
<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox } from 'vectis-ui'

const chips = ref(['fr', 'be', 'ch', 'lu', 're'])
const text = ref(['fr', 'be', 'ch', 'lu', 're'])

const countries = [
  { value: 'be', label: 'Belgium' },
  { value: 'ca', label: 'Canada' },
  { value: 'ci', label: "Côte d'Ivoire" },
  { value: 'fr', label: 'France' },
  { value: 'lu', label: 'Luxembourg' },
  { value: 're', label: 'Réunion' },
  { value: 'ch', label: 'Switzerland' },
]
</script>

<template>
  <div class="column">
    <VCombobox
      v-model="chips"
      :options="countries"
      multiple
      :max="2"
      label="Served countries"
      hint="Two chips, the rest counted until the field is focused"
    />
    <VCombobox
      v-model="text"
      :options="countries"
      multiple
      display="text"
      :max="2"
      :overflow-text="(count) => `+${count} countries`"
      label="Served countries, as text"
    />
  </div>
</template>

<style scoped>
.column {
  display: grid;
  gap: 1rem;
  max-inline-size: 20rem;
}
</style>

Field icon

iconStart displays an icon at the beginning of the field. iconStartLabel provides an accessible label if the icon is made interactive.

vue
DesignAccessibility

Sorted A to Z.

<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox } from 'vectis-ui'
import { search, swap_vert as swapVert } from 'vectis-ui/icons'

const topic = ref('')
const tags = ref(['design', 'a11y'])
const order = ref<'A to Z' | 'Z to A'>('A to Z')

const options = [
  { value: 'design', label: 'Design' },
  { value: 'a11y', label: 'Accessibility' },
  { value: 'perf', label: 'Performance' },
  { value: 'docs', label: 'Documentation' },
]

function switchOrder() {
  order.value = order.value === 'A to Z' ? 'Z to A' : 'A to Z'
}
</script>

<template>
  <div class="fields">
    <VCombobox
      v-model="topic"
      :options="options"
      :icon-start="search"
      label="Decorative icon"
      placeholder="Search a topic"
    />

    <!-- The icon is rendered before the chips, so a multiple field keeps both. -->
    <VCombobox
      v-model="tags"
      :options="options"
      multiple
      :icon-start="search"
      label="Beside the chips"
    />

    <VCombobox
      v-model="topic"
      :options="options"
      :icon-start="swapVert"
      icon-start-label="Switch the order"
      label="Clickable icon"
      :hint="`Sorted ${order}.`"
      placeholder="Search a topic"
      @click:icon-start="switchOrder"
    />
  </div>
</template>

<style scoped>
.fields {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Option icons

An option's icon property displays an icon alongside its label in the dropdown list.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox, type ComboboxOption } from 'vectis-ui'
import {
  description,
  folder_zip as folderZip,
  image,
  picture_as_pdf as pictureAsPdf,
  table_chart as tableChart,
  video_file as videoFile,
} from 'vectis-ui/icons'

const type = ref('img')

/* `icon` takes what every icon prop in the library takes. A row without one starts
   straight at its label rather than reserving a blank column. */
const types: ComboboxOption[] = [
  { value: 'doc', label: 'Document', icon: description },
  { value: 'img', label: 'Image', icon: image },
  { value: 'vid', label: 'Video', icon: videoFile },
  { value: 'pdf', label: 'PDF', icon: pictureAsPdf },
  { value: 'zip', label: 'Archive', icon: folderZip },
  { value: 'raw', label: 'Anything else' },
  { value: 'xls', label: 'Spreadsheet', icon: tableChart, disabled: true },
]
</script>

<template>
  <div class="column">
    <VCombobox v-model="type" :options="types" label="File type" placeholder="Choose a type" />
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
</style>

Disabling filter shows options exactly as provided by the source. searchDebounce sets the delay in milliseconds before emitting the search term.

vue

The list is narrowed by the source, so local filtering is turned off

<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox, type ComboboxOption } from 'vectis-ui'

const CATALOGUE: ComboboxOption[] = Array.from({ length: 120 }, (_, i) => ({
  value: `ref-${i + 1}`,
  label: `Reference ${String(i + 1).padStart(3, '0')}`,
}))

/* Stands in for a server: latency, and the filtering done on its side. */
function fetchReferences(query: string): Promise<ComboboxOption[]> {
  const found = CATALOGUE.filter((option) =>
    option.label.toLowerCase().includes(query.toLowerCase()),
  )
  return new Promise((resolve) => setTimeout(() => resolve(found.slice(0, 20)), 400))
}

const reference = ref('')
const options = ref<ComboboxOption[]>([])
const loading = ref(false)

/* A token, so a slow answer to an old keystroke cannot overwrite a fresh one. */
let latest = 0

async function onSearch(query: string) {
  const current = ++latest
  loading.value = true
  const found = await fetchReferences(query)
  if (current !== latest) return
  options.value = found
  loading.value = false
}
</script>

<template>
  <div class="column">
    <VCombobox
      v-model="reference"
      :options="options"
      :loading="loading"
      :filter="false"
      label="Reference"
      hint="The list is narrowed by the source, so local filtering is turned off"
      placeholder="Search for a reference"
      empty-text="No reference matches"
      clearable
      @search="onSearch"
    />
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
</style>

Infinite scroll

hasMore indicates that more pages are available, triggering a load-more event when the end of the list comes into view.

vue

Scroll to the foot of the list: the next page is asked for as the end comes into view

0 loaded of 0
<script setup lang="ts">
import { computed, ref } from 'vue'
import { VCombobox, type ComboboxOption } from 'vectis-ui'

const PAGE_SIZE = 20

const CATALOGUE: ComboboxOption[] = Array.from({ length: 120 }, (_, i) => ({
  value: `ref-${i + 1}`,
  label: `Reference ${String(i + 1).padStart(3, '0')}`,
}))

function fetchPage(query: string, page: number) {
  const found = CATALOGUE.filter((option) =>
    option.label.toLowerCase().includes(query.toLowerCase()),
  )
  return new Promise<{ items: ComboboxOption[]; total: number }>((resolve) =>
    setTimeout(
      () =>
        resolve({
          items: found.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE),
          total: found.length,
        }),
      400,
    ),
  )
}

const reference = ref('')
const options = ref<ComboboxOption[]>([])
const loading = ref(false)
const total = ref(0)
const page = ref(0)
const query = ref('')

const hasMore = computed(() => options.value.length < total.value)

async function onSearch(term: string) {
  query.value = term
  page.value = 0
  loading.value = true
  const result = await fetchPage(term, 0)
  options.value = result.items
  total.value = result.total
  loading.value = false
}

/* The component asks once per page and waits: the next request only goes out when the
   sentinel comes back into view, which it cannot do until this one has landed. */
async function onLoadMore() {
  loading.value = true
  const result = await fetchPage(query.value, page.value + 1)
  page.value += 1
  options.value = [...options.value, ...result.items]
  loading.value = false
}
</script>

<template>
  <div class="column">
    <VCombobox
      v-model="reference"
      :options="options"
      :loading="loading"
      :has-more="hasMore"
      :filter="false"
      label="Reference"
      hint="Scroll to the foot of the list: the next page is asked for as the end comes into view"
      placeholder="Search for a reference"
      @search="onSearch"
      @load-more="onLoadMore"
    />
    <output class="count" aria-label="Options loaded"
      >{{ options.length }} loaded of {{ total }}</output
    >
  </div>
</template>

<style scoped>
.column {
  display: grid;
  gap: var(--vectis-space-3);
  max-inline-size: 26rem;
}
.count {
  font-family: var(--vectis-text-family-code);
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-text-muted);
}
</style>

Custom options

The #option slot allows customizing the content and layout of an option row (e.g., adding a badge or a second line).

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VCombobox } from 'vectis-ui'

const country = ref('fr')

const countries = [
  { value: 'be', label: 'Belgium' },
  { value: 'ca', label: 'Canada' },
  { value: 'fr', label: 'France' },
  { value: 'lu', label: 'Luxembourg' },
  { value: 'ch', label: 'Switzerland' },
]

const capitals: Record<string, string> = {
  be: 'Brussels',
  ca: 'Ottawa',
  fr: 'Paris',
  lu: 'Luxembourg',
  ch: 'Bern',
}
</script>

<template>
  <div class="column">
    <VCombobox v-model="country" :options="countries" label="Country">
      <template #option="{ option, selected }">
        <span class="row">
          <span>{{ option.label }}</span>
          <small class="capital"
            >{{ capitals[option.value] }}{{ selected ? ' · chosen' : '' }}</small
          >
        </span>
      </template>
    </VCombobox>
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
/* The slot replaces the label, so its content is laid out inside the row the panel
   already spaces and aligns. */
.row {
  display: grid;
}
.capital {
  color: var(--vectis-color-text-muted);
}
</style>

Custom chips

The #chip slot allows customizing the appearance of the selected value chips.

vue
DocumentImage
<script setup lang="ts">
import { ref } from 'vue'
import { VChip, VCombobox, type ComboboxOption } from 'vectis-ui'
import {
  description,
  image,
  picture_as_pdf as pictureAsPdf,
  video_file as videoFile,
} from 'vectis-ui/icons'

const chosen = ref(['doc', 'img'])

const types: ComboboxOption[] = [
  { value: 'doc', label: 'Document', icon: description },
  { value: 'img', label: 'Image', icon: image },
  { value: 'vid', label: 'Video', icon: videoFile },
  { value: 'pdf', label: 'PDF', icon: pictureAsPdf },
]
</script>

<template>
  <div class="column">
    <VCombobox
      v-model="chosen"
      :options="types"
      multiple
      label="File types"
      placeholder="Add a type"
    >
      <!--
        `size` and `compact` are the step the field worked out for its chips, which cannot
        be guessed from out here, and `remove` is what keeps the value removable.
      -->
      <template #chip="{ option, label, remove, size, compact }">
        <VChip
          :icon-start="option?.icon"
          :size="size"
          :compact="compact"
          :dismiss-label="`Remove ${label}`"
          variant="outline"
          tone="accent"
          dismissible
          @dismiss="remove"
        >
          {{ label }}
        </VChip>
      </template>
    </VCombobox>
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
</style>

API

Props

PropTypeDefault
optionsComboboxItem[]none
What the list offers. An entry may be an option, a named block of options, or a separator; a plain list of options remains perfectly valid.
multiplebooleanfalse
Allows several values to be chosen, which makes the value a list and shows what has been chosen inside the field, as chips or as text depending on display.
displayComboboxDisplay'chip' | 'text''chip'
How the chosen values are shown when several can be chosen: one dismissible chip each, or their labels joined by commas on a single line, cut short with an ellipsis. It changes nothing for a single value, which is always text.
maxnumbernone
How many chosen values to show before the rest are summed up as "+X", as chips or as text. It applies while the field is out of focus; focused, every value comes back so it can be seen and removed. Left out, or set to 0, every value is shown. It changes nothing without multiple.
overflowText(count: number) => stringnone
Rephrases the "+X" standing for the values beyond max, "+5 products" for instance. It receives the number of values being hidden.
labelstringnone
The label above the field, tied to it so that clicking it focuses the field.
hintstringnone
A line of help under the field, read out along with the label.
sizeComboboxSize'sm' | 'md' | 'lg''md'
The height of the field: 32, 40 or 48 pixels. The panel and its rows follow it.
compactbooleanfalse
Takes 4px off the height, as everywhere else in the design system.
placeholderstringnone
What the field says while nothing is chosen and nothing has been typed.
disabledbooleanfalse
Makes the field unusable, greyed out through the colour tokens.
readonlybooleanfalse
Shows what has been chosen without letting it be changed: nothing can be typed, the list never opens, the chips lose their crosses and no clear cross is offered. The field keeps the focus and can be copied from, which is what separates it from disabled.
invalidbooleanfalse
Marks the field as invalid, for a rule of your own.
iconStartIconSourcenone
An icon inside the field, at the start. It is rendered before the chips rather than in their place. Decorative until a @click:icon-start listener turns it into a button.
iconStartLabelstringnone
What the start icon does, in words, once it is clickable.
expandIconIconSourceexpand_more
The chevron at the end of the field, which turns as the list opens. Clicking it while the list is open closes the list. It is decoration all the same: the field itself opens the list and Escape closes it from the keyboard, so the chevron is hidden from screen readers and takes no label.
clearablebooleanfalse
Offers a cross that empties both the selection and the search.
clearLabelstringnone
What that cross does, in words. It falls back to the design system dictionary.
emptyTextstringnone
What the panel says when the search matches nothing. A screen reader hears it even when the #empty slot draws something else, so set both together.
filterComboboxFiltertrue
How the list is narrowed as one types. Turning it off means the options already arrive filtered by their source and are shown exactly as they come. A rule of your own receives the query as it was typed, merely trimmed, not the accent-insensitive form used internally.
searchDebouncenumber250
How long to wait before telling the source what is being searched for, in milliseconds. Zero tells it at once, which suits a source that is not a network request.
loadingbooleanfalse
Says that something is being loaded. With no option yet, the whole panel says so; with options already listed, a spinner appears at the foot of the list, since what is loading is then the next page. Either way the field replaces its chevron with a spinner.
loadingTextstringnone
What is said while loading, and what the spinner is announced as. A screen reader hears it even when the #loading slot draws something else, so set both together.
hasMorebooleanfalse
Says that there are more pages to come, which is what makes the component ask for the next one as the end of the list comes into view.
placementComboboxPlacement'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end''bottom-start'
Where the list opens relative to the field. The panel is anchored in CSS, so this names a preference: a browser short of room already falls back on its own.
v-modelItemValue | ItemValue[]''
The chosen option's value, or the list of them when multiple is set. It is an empty string to begin with, and the array is never mutated in place.

Events

EventType
search[query: string]
What is being searched for, to be sent to the source. It is delayed by searchDebounce while typing, and emitted at once when the panel opens so that a first page can be loaded. The same term is never emitted twice in a row.
click:icon-start[event: MouseEvent]
The start icon was clicked. Attaching this listener is what turns that icon into a real button, which then needs iconStartLabel.
clear[]
The clear cross emptied the selection and the search.
load-more[]
The end of the list has come into view: send the next page.

Slots

SlotType
start{}
Content at the start of the field, rendered after iconStart rather than in its place.
value-end{}
Controls of your own inside the field, placed before the ones the field owns: the clear cross and the icon that opens the panel. Those two are the component own affordance, which is why there is no end slot here.
optionComboboxOptionSlotProps
What a row of the list shows, in place of the plain label: a subtitle, an avatar, a badge. It is told whether the row is the highlighted one and whether it is already chosen.
chipComboboxChipSlotProps
Replaces the chip standing for one chosen value. It receives remove, without which the value could no longer be taken back, and the size and density worked out to sit inside the field, which cannot be guessed from outside. The option itself may be missing, if that value has never appeared among the options.
overflowComboboxOverflowSlotProps
Replaces the "+X" standing for the values beyond max. It receives count, the number of values being hidden, and the size and density of the chips inside the field, so that a chip of your own lines up with the others.
emptyComboboxEmptySlotProps
What the panel shows when nothing matches. It receives the term that was searched.
loading{}
What the panel shows while loading its first options.

Types

The types the tables above name, written as the library declares them. The ones carrying export can be imported from vectis-ui to type your own code; the others are the shape of what a slot hands out.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export type ChipSize = 'xs' | 'sm'
export interface ComboboxChipSlotProps {
  value: ItemValue
  option: ComboboxOption | undefined
  label: string
  remove: () => void
  size: ChipSize
  compact: boolean
}
export interface ComboboxEmptySlotProps {
  query: string
}
export type ComboboxFilter = boolean | ((option: ComboboxOption, query: string) => boolean)
export interface ComboboxGroup {
  label: string
  options: ComboboxOption[]
}
export type ComboboxItem = ComboboxOption | ComboboxGroup | ComboboxSeparator
export interface ComboboxOption {
  value: ItemValue
  label: string
  icon?: IconSource
  disabled?: boolean
}
export interface ComboboxOptionSlotProps {
  option: ComboboxOption
  index: number
  active: boolean
  selected: boolean
}
export interface ComboboxOverflowSlotProps {
  count: number
  size: ChipSize
  compact: boolean
}
export interface ComboboxSeparator {
  separator: true
}
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender
export type ItemValue = string | number

CSS variables

TokenValue
--vectis-control-size-combobox-list-max-block18rem