Skip to content

VirtualSelect

A searchable select input backed by a virtualized dropdown. Handles hundreds of thousands of options without DOM overhead.

Props

options

T[]

Option objects.

modelValue

T | null · default: null

Currently selected option.

labelField

string · default: 'label'

Field to display in the trigger and dropdown.

valueField

string · default: 'value'

Field used for value comparison — also used as the dropdown's row key, so it should be unique across options.

placeholder

string · default: 'Select an option…'

Placeholder text.

disabled

boolean · default: false

Disable the select.

clearable

boolean · default: false

Show a clear button when a value is selected.

searchable

boolean · default: true

Show a search input when the dropdown opens.

estimatedItemSize

number · default: 36

Estimated option row height.

maxVisibleRows

number · default: 8

Max rows shown before the dropdown scrolls.

motionBlur

boolean · default: false

Apply a CSS blur that scales with scroll velocity while scrolling fast.

remote

boolean · default: false

Skip client-side filtering — options is rendered as-is; you update it yourself in response to search.

debounceMs

number · default: 0

Delay before the search event fires after typing stops (coalesces keystrokes for a server round-trip). 0 keeps today's synchronous behavior.

isLoading

boolean · default: false

Shows the #loading slot in the dropdown, checked before the #empty slot so an in-flight remote search doesn't flash "No options".

Emits

update:modelValue

Payload: T | null

change

Payload: T | null

Payload: string

Slots

default

Scope: { option: T, index: number, selected: boolean }

Custom option row.

empty

Scope: No scope

Shown when filteredOptions is empty and not loading.

loading

Scope: No scope

Shown while isLoading is true, in place of the option list.

Exposed API

open()

Opens the dropdown.

close()

Closes the dropdown.

getScrollElement()

Returns the element that actually scrolls — pair with VirtualScrollbar.

ts
selectRef.value?.open()
selectRef.value?.close()
selectRef.value?.getScrollElement() // pair with VirtualScrollbar

Example

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VirtualSelect } from 'vue-virtual-scroller-kit'

interface Country {
  value: string
  label: string
  flag: string
}

const countries: Country[] = [
  { value: 'us', label: 'United States', flag: '🇺🇸' },
  { value: 'de', label: 'Germany', flag: '🇩🇪' },
  // … hundreds more
]

const selected = ref<Country | null>(null)
</script>

<template>
  <VirtualSelect
    v-model="selected"
    :options="countries"
    label-field="label"
    value-field="value"
    clearable
    style="width: 300px"
  >
    <template #default="{ option }"> {{ option.flag }} {{ option.label }} </template>
  </VirtualSelect>
</template>

Async/remote searchoptions is populated from a server, filtering happens there instead of client-side:

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VirtualSelect } from 'vue-virtual-scroller-kit'

interface Country {
  value: string
  label: string
}

const selected = ref<Country | null>(null)
const results = ref<Country[]>([])
const isLoading = ref(false)
let requestId = 0

async function onSearch(query: string) {
  const id = ++requestId
  isLoading.value = true
  try {
    const res = await fetch(`/api/countries?q=${encodeURIComponent(query)}`)
    const data: Country[] = await res.json()
    if (id !== requestId) return // a newer keystroke already fired another request
    results.value = data
  } finally {
    if (id === requestId) isLoading.value = false
  }
}
</script>

<template>
  <VirtualSelect
    v-model="selected"
    :options="results"
    remote
    :debounce-ms="300"
    :is-loading="isLoading"
    label-field="label"
    value-field="value"
    style="width: 300px"
    @search="onSearch"
  >
    <template #loading>Searching…</template>
  </VirtualSelect>
</template>