Skip to content

<VSelect> API Reference

Props

Core

PropTypeDefaultDescription
modelValuestring | number | (string | number)[] | nullundefinedBound via v-model. Array in multiple mode.
optionsReadonlyArray<SelectOption>(required)Option tree (see SelectOption).
labelstringVisible label. Also drives the listbox's accessible name.
placeholderstring'Select...'Text shown when nothing is selected.
multiplebooleanfalseEnables multi-select with tags.
searchablebooleanfalseRenders a text input that filters options.
disabledbooleanfalseDisables the entire control.
clearablebooleanfalseAdds a clear (×) button when a value is selected.
requiredbooleanfalseSets aria-required="true" and (with validateOnSubmit) blocks form submission while empty.
validateOnSubmitbooleantrueEnables native HTML5 form validation. Set to false to delegate to your own validation library.
validationMessagestring'Please select an item.'Custom message for the native validation tooltip.
errorstringError message; sets aria-invalid and links via aria-describedby.
idstringauto (SSR-safe via useId)Root id; used to derive the trigger, listbox, label, error and status ids.
namestringWhen set, hidden <input>s are emitted for native form submission.
size'sm' | 'md' | 'lg''md'Visual size variant.
autocompletestring'off'Forwarded to the searchable input. Use 'country', 'email', etc. for browser autofill.
inputmode'text' | 'search' | 'numeric' | 'email' | ...Forwarded to the searchable input as a soft-keyboard hint on mobile.

Search & async

PropTypeDefaultDescription
filterablebooleantrueDisable client-side filtering when handling search server-side.
minSearchLengthnumber0Suppress @search and show a hint until the query reaches this length.
searchDebouncenumber (ms)0Debounce window for the @search event. The input itself stays responsive.
loadingbooleanfalseShows a spinner, sets aria-busy="true", announces "Loading…".

Positioning

PropTypeDefaultDescription
placementPlacement'bottom-start'Preferred placement; auto-flips when no space is available.
teleportboolean | string | HTMLElementtruetrue teleports the menu to <body>; false renders inline; a string is treated as a CSS selector.
maxMenuHeightnumber (px)320Hard cap on menu height; viewport bounds always win.
itemHeightnumber (px)40Row height used by the virtualizer.

Localization

PropTypeDescription
listboxLabelstringOverrides the listbox's accessible label. Falls back to label.
labelsVSelectLabelsOverride every screen-reader string (see below).
ts
interface VSelectLabels {
    clear?: string;                          // "Clear selection"
    removeItem?: (label: string) => string;  // "Remove Apple"
    noResults?: string;                      // "No results."
    addChild?: string;                       // "Add child item"
    resultsCount?: (n: number) => string;    // "3 results available."
    loading?: string;                        // "Loading…"
    typeToSearch?: (min: number) => string;  // "Type at least 2 characters to search."
}

Events

EventPayloadDescription
update:modelValueSelectModelValueStandard v-model update.
openMenu opened.
closeMenu closed (any cause).
searchstringDebounced search query. Use with :filterable="false" for server-side search.
create{ parent, value }Creator-mode submission.

Exposed methods

Accessible via template ref (ref="mySelect"mySelect.value.open()):

MethodDescription
open()Open the listbox.
close()Close the listbox.
focus()Move focus back to the trigger (button or input, depending on searchable).
clear()Reset the model (undefined in single mode, [] in multi).

Slots

SlotWhereScopeDescription
valuetrigger{ value, label }Replaces the displayed single value.
optionmenu{ option }Replaces an option's label cell.
groupmenu{ group }Replaces a group header.
emptymenuRendered when visibleOptions is empty (after loading/min-search states).
loadingmenuReplaces the in-menu "Loading…" state.
loading-icontriggerReplaces the spinner glyph next to the trigger (different slot, same word).
hintmenu{ min }Replaces the "type at least X characters" hint.
trigger-icontrigger{ isOpen }Replaces the chevron in the trigger.
toggle-iconmenu{ collapsed, option }Replaces the tree expand/collapse glyph.
add-iconmenu{ option }Replaces the "+" glyph next to expandable rows.
creatormenu{ cancel }Replaces the inline create-input row.

Keyboard

KeyEffect
/ Move highlight to next/previous option (wraps).
Alt + / Open / close menu.
PageDown / PageUpJump 10 items.
Home / EndFirst / last option.
Expand current tree node.
Collapse current tree node, or jump to its parent.
Enter / SpaceSelect the highlighted option.
BackspaceEmpty search input. Single mode: clear the selected value. Multi mode: remove the last selected tag.
EscapeClose the menu, return focus to the trigger.
TabClose the menu and continue tab order.

Behaviour notes

Trigger surface and Tab focus

The element that receives keyboard focus depends on searchable:

searchableFocus targetDOMComment
false<button role="combobox">hidden via position: absolute; clip: …The "trigger surface" you see is the <div class="vue-select-control"> wrapper. Clicks anywhere on it programmatically focus the button.
true<input role="combobox">always visible while menu is open; visually hidden while menu is closed and a single value is selected (the value span owns the row)Click on the wrapper focuses the input.

Logic gated on searchable (e.g. "auto-open on focus") should target the input ref returned from useTemplateRef/ref, not the button.

Initial highlight on open

When the menu opens, the highlight lands on:

  • the currently selected option (single mode) or the first selected option (multi mode), if any;
  • otherwise the first navigable row (groups, disabled rows and the creator placeholder are skipped).

This is the "select-with-cursor" variant of the WAI-ARIA combobox pattern — the default <select> behaviour. The Authoring Practices Guide also documents a "no initial highlight" variant; that is not what this component does. The next moves to the option after the highlight, not to the first one.

itemHeight and custom option slots

The virtualizer needs to know the row height to compute scroll position and aria-setsize. itemHeight (default 40 px) must match the actual rendered height of an option row. If you replace the option slot with multi-line content, bump itemHeight to match — otherwise the scroll position drifts and rows partially clip.

For mixed-height rows (some single-line, some multi-line) the virtualizer is not the right tool — disable virtualization by capping the option count instead, or render your own list using useSelect() headless.

labels is partial-override

The labels prop is merged key-by-key with the defaults — any key you omit keeps its English default. There is no deep-merge inside the callback signatures.

ts
:labels="{ clear: 'Leeren' }"
// → clear: 'Leeren', all other keys still use their English defaults

Accessibility

The component implements the WAI-ARIA 1.2 combobox pattern:

  • role="combobox" on the focusable element (<button> or <input>), with aria-haspopup="listbox", aria-expanded, aria-controls, aria-activedescendant.
  • aria-autocomplete="list" when searchable.
  • role="listbox" on the popup, with aria-multiselectable in multi mode.
  • Each option has role="option", aria-selected, aria-setsize, aria-posinset, aria-level, and (for tree nodes) aria-expanded.
  • A polite live region announces result counts, loading, and below-min-search hints.
  • Tag-remove buttons are reachable via Tab with aria-label="Remove {label}".
  • Errors are linked via aria-invalid + aria-describedby.
  • All animations respect prefers-reduced-motion.
  • Windows High Contrast / forced-colors is fully styled with system colours.

SelectOption

ts
interface SelectOption {
    value?: string | number;  // required for selectable rows; omit for group-only headers
    label: string;
    disabled?: boolean;
    children?: SelectOption[];
    group?: string;           // when set, this row renders as a non-selectable group header
}

CSS variables

Override on :root, .dark, or any ancestor. Defaults shown below.

css
:root {
    --vs-primary:        #2563eb;
    --vs-primary-hover:  #1d4ed8;
    --vs-danger:         #dc2626;
    --vs-bg:             #ffffff;
    --vs-bg-elevated:    #ffffff;
    --vs-bg-subtle:      #f9fafb;
    --vs-bg-hover:       #f3f4f6;
    --vs-text:           #111827;
    --vs-text-muted:     #6b7280;
    --vs-border:         #d1d5db;
    --vs-border-hover:   #9ca3af;
    --vs-selected-bg:    #dbeafe;
    --vs-selected-text:  #1e40af;
    --vs-tag-bg:         #eef2ff;
    --vs-tag-text:       #3730a3;
    --vs-focus-ring:     0 0 0 3px rgba(37, 99, 235, 0.45);
    --vs-radius:         6px;
    --vs-z-menu:         50;
    --vs-control-height: 2.5rem;
    --vs-font-size:      0.875rem;
}

Theming with classes

A dark theme kicks in automatically via prefers-color-scheme. To force a theme, set the light or dark class on any ancestor.

If your app already uses .dark for unrelated theming (e.g. Tailwind), use the namespaced .vsp-dark class instead — both are supported and equivalent.

html
<!-- Either of these works -->
<html class="dark">      <!-- collides with Tailwind's .dark mode -->
<html class="vsp-dark">  <!-- recommended when Tailwind is in play -->