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
search
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.
selectRef.value?.open()
selectRef.value?.close()
selectRef.value?.getScrollElement() // pair with VirtualScrollbarExample
<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 search — options is populated from a server, filtering happens there instead of client-side:
<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>