Keyboard shortcut: Ctrl + K
Get started

Select

VSelect chooses one or more values from a list, like a native <select>, with the look of VCombobox. There is nothing to type: letters typed on the field highlight the matching option.

Usage

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VSelect } 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>
  <VSelect
    v-model="country"
    :options="options"
    aria-label="Country"
    placeholder="Choose a country"
  />
</template>

Examples

Label and hint

Use label to name the field and hint for help text. Clicking the label opens the list.

vue

The country shown on your invoices

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

const country = ref('')

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

<template>
  <div class="column">
    <VSelect
      v-model="country"
      :options="countries"
      label="Billing country"
      hint="The country shown on your invoices"
      placeholder="Choose a country"
    />
  </div>
</template>

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

Sizes

size adjusts the field and option rows. compact reduces the field height.

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

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

const rows = (['sm', 'md', 'lg'] as const).flatMap((size) => [
  { key: size, size, compact: false, label: size },
  { key: `${size}-compact`, size, compact: true, label: `${size}, compact` },
])
</script>

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

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

States

Use disabled, readonly and invalid for field states, and clearable to clear the selection. A read-only field stays focusable but its list never opens.

vue

The cross empties the selection

For a rule the browser cannot check by itself

Set by your subscription

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

const chosen = ref('fr')

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

<template>
  <div class="column">
    <VSelect
      v-model="chosen"
      :options="countries"
      clearable
      label="Clearable"
      hint="The cross empties the selection"
      placeholder="Choose a country"
    />

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

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

    <VSelect
      :options="countries"
      model-value="fr"
      readonly
      label="Read-only"
      hint="Set by your subscription"
    />
  </div>
</template>

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

Groups and separators

options accepts options, named groups and separators. The keyboard steps over groups and separators.

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

const country = ref('fr')

const items: SelectItem[] = [
  {
    label: 'Europe',
    options: [
      { value: 'fr', label: 'France' },
      { value: 'be', label: 'Belgium' },
      { value: 'ch', label: 'Switzerland' },
    ],
  },
  {
    label: 'America',
    options: [
      { value: 'ca', label: 'Canada' },
      { value: 'us', label: 'United States' },
      { value: 'br', label: 'Brazil' },
    ],
  },
  { separator: true },
  { value: 'other', label: 'Other' },
]
</script>

<template>
  <div class="column">
    <VSelect v-model="country" :options="items" label="Country" />
  </div>
</template>

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

Multiple selection

multiple uses an array for the selection and displays removable chips. The list stays open while options are toggled. max limits visible values while the field is unfocused.

vue
FranceBelgium+1

Two values are shown while the field is not focused

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

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

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

<template>
  <div class="column">
    <VSelect
      v-model="served"
      :options="countries"
      multiple
      clearable
      :max="2"
      label="Served countries"
      hint="Two values are shown while the field is not focused"
      placeholder="Choose countries"
    />
    <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" joins selected labels on one line, cut short with an ellipsis.

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

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

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

<template>
  <div class="column">
    <VSelect
      v-model="served"
      :options="countries"
      multiple
      display="text"
      label="Served countries"
      placeholder="Choose countries"
    />
  </div>
</template>

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

Custom options

Use #option to customize option content.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VSelect } 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">
    <VSelect v-model="country" :options="countries" label="Country">
      <template #option="{ option }">
        <span class="row">
          <span>{{ option.label }}</span>
          <small class="capital">{{ capitals[option.value] }}</small>
        </span>
      </template>
    </VSelect>
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
.row {
  display: grid;
}
.capital {
  color: var(--vectis-color-text-muted);
}
</style>

Forms

A hidden native <select> receives name, form, required and autocomplete. The browser validates it on submission, autofill updates v-model, and a form reset restores the initial value.

vue
nothing yet
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VSelect } from 'vectis-ui'

const country = ref('')
const submitted = ref('')

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

function onSubmit(event: Event) {
  const data = new FormData(event.target as HTMLFormElement)
  submitted.value = `country=${data.get('country')}`
}

function onReset() {
  submitted.value = ''
}
</script>

<template>
  <form class="column" @submit.prevent="onSubmit" @reset="onReset">
    <VSelect
      v-model="country"
      :options="countries"
      name="country"
      required
      label="Country"
      placeholder="Choose a country"
    />
    <div class="actions">
      <VButton type="submit">Submit</VButton>
      <VButton type="reset" variant="outline">Reset</VButton>
    </div>
    <output class="value" aria-label="Submitted data">{{ submitted || 'nothing yet' }}</output>
  </form>
</template>

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

API

Props

PropTypeDefault
optionsSelectItem[]none
Options, named groups or separators. Each option has a value and label.
multiplebooleanfalse
Allows multiple selection. Use an array for v-model. The list stays open while options are toggled.
displaySelectDisplay'chip' | 'text''chip'
Multiple-selection display: removable chips or comma-separated text. Single selection always uses text.
maxnumbernone
Visible selection count while unfocused. Focus shows all values. Omitted or zero shows all values. Applies only with multiple.
overflowText(count: number) => stringnone
Formats the count of values hidden by max. Receives the hidden count.
labelstringnone
Visible label. Without a visible name, provide aria-label or aria-labelledby.
hintstringnone
Help text linked through aria-describedby.
errorstringnone
Error message shown in place of the hint. Sets aria-invalid, is linked through aria-describedby and is announced when it appears.
sizeSelectSize'sm' | 'md' | 'lg''md'
Field and option size. The field inherits a size set by VInputGroup.
compactbooleanfalse
Reduces the control height without changing text or icons.
placeholderstringnone
Text shown while nothing is chosen.
disabledbooleanfalse
Disables interaction. The value is not submitted.
readonlybooleanfalse
Prevents selection changes. Keeps focus and submission; hides clear actions and prevents opening the list.
invalidbooleanfalse
Sets aria-invalid and the error style. Does not block form submission by itself.
iconStartIconSourcenone
Leading icon before selected values. A @click:icon-start listener makes it a button requiring iconStartLabel.
iconStartLabelstringnone
Accessible name of the start icon button.
expandIconIconSourceexpand_more
Decorative chevron, turned while the list is open.
clearablebooleanfalse
Adds an action to clear the selection.
clearLabelstringnone
Accessible name of the clear button. Defaults to the library dictionary.
placementSelectPlacement'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end''bottom-start'
Preferred panel position relative to the field.
v-modelItemValue | ItemValue[]''
Selected string or number, or an array with multiple. Defaults to an empty string.

Events

EventType
click:icon-start[event: MouseEvent]
Emitted when the start icon button is activated.
clear[]
Emitted after clearing the selection.

Slots

SlotType
start{}
Content after iconStart and the chips, without replacing them.
value-end{}
Content before the clear action and expand icon.
optionListboxOptionSlotProps
Option content. Receives the option, index, active state and selected state.
chipListboxChipSlotProps
Selected chip. Receives value, label, optional option, remove, size and compact state. Connect the remove action.
overflowListboxOverflowSlotProps
Hidden selection count. Receives count, chip size and compact state.

Types

Types used in the API tables. Import exported types from vectis-ui.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export type ChipSize = 'xs' | 'sm'
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
export interface ListboxChipSlotProps {
  value: ItemValue
  option: ListboxOption | undefined
  label: string
  remove: () => void
  size: ChipSize
  compact: boolean
}
export interface ListboxGroup {
  label: string
  options: ListboxOption[]
}
export type ListboxItem = ListboxOption | ListboxGroup | ListboxSeparator
export interface ListboxOption {
  value: ItemValue
  label: string
  icon?: IconSource
  disabled?: boolean
}
export interface ListboxOptionSlotProps {
  option: ListboxOption
  index: number
  active: boolean
  selected: boolean
}
export interface ListboxOverflowSlotProps {
  count: number
  size: ChipSize
  compact: boolean
}
export interface ListboxSeparator {
  separator: true
}
export type SelectItem = ListboxItem

CSS variables

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