Search
searchable swaps the button trigger for an input with built-in type-ahead filtering. The list shows everything when opened, and narrows as the user types.
Every keystroke also emits search with the current query, if you need to react to it.
Clearable
clearable shows a clear button once a value is selected. It works in both button and searchable modes.
The clear button wipes the whole value in one go and emits only update:modelValue. It does not emit deselect, so watch the model if you need to react to a clear.
In searchable mode Escape does double duty: it closes an open list, and when the list is already closed and clearable is set, it clears the selection.
Custom matching
The built-in matcher is a substring test against the label that ignores case and accents, so "cafe" finds "Café" and "MÜLLER" finds "Muller". Pass filter to replace it. It receives each option and the current query, and returns whether that option survives:
<script setup>
// Match on a code field as well as the label, and only from the start.
function filter(option, query) {
const q = query.toLowerCase()
return (
option.label.toLowerCase().startsWith(q) ||
String(option.raw.code).toLowerCase().startsWith(q)
)
}
</script>
<template>
<VPick :options="airports" searchable :filter="filter" />
</template>The option handed in is Vue Pick's normalized shape, so option.label and option.value are always there whatever your labelKey and valueKey say, and option.raw is the exact object you passed in options. The query arrives trimmed but with its case intact, so lowercase it yourself if you want a case-insensitive match.
In tree mode the filter runs per node, and the branch rules are applied on top of whatever it returns. A branch is kept when it matches or when something under it does, so you do not have to walk children yourself.
filter replaces the whole matcher, searchNested included. If you set both, only filter runs, and matching the ancestor path is then yours to implement.
Searching other fields
The built-in search looks at the label. searchKeys names more fields to look at, read straight from your option objects. The label still counts:
<VPick
:options="people"
label-key="name"
value-key="id"
:search-keys="['email', 'tags']"
searchable
/>With that, typing "bletchley" finds { name: "Alan Turing", email: "alan@bletchley.uk" }. A field can hold a string, a number, or an array of either, such as a list of tags. Case and accents are ignored in every field, and a tree opens the path to a node that matched on one of them.
A custom filter replaces the built-in search entirely, so searchKeys has no effect alongside it.
After picking
Two props decide what happens once an option is chosen.
clearOnSelect (default true) empties the search query, so the next search starts clean rather than from the text that found the last pick.
closeOnSelect decides whether the list closes. Left unset it follows the mode: true in single-select, false in multiple, which is what each one usually wants. Setting it explicitly applies to both, so the prop never silently does nothing.
<!-- Single-select that stays open, for picking through a long list -->
<VPick :options="options" searchable :close-on-select="false" />Empty states
Two different situations, two messages:
noOptionsText(default"No options available") whenoptionsis emptynoResultsText(default"No results") when a search matches nothing
<VPick
:options="options"
searchable
no-options-text="Nothing to pick from yet"
no-results-text="No match for that"
/>The empty slot replaces whichever applies, and receives the current query so the message can quote it.
Searching across the ancestor path
By default a query is matched against each node's own label. searchNested widens that to the node's full ancestor path:
<VPick :options="options" searchable search-nested />Searching "electronics gaming" finds Gaming under Electronics > Laptops, because every word appears somewhere in that path. Single-word queries are unaffected, and words from unrelated branches still do not match.
Only useful in tree mode. See Tree Select for how search interacts with branches and expansion.
Searching a server
When the options live on a server, pass fetchOptions instead of filtering options in the browser. It receives what the user typed and returns a promise of the matching options, in the same shape as the rest:
<script setup>
async function fetchOptions(query, { signal }) {
const res = await fetch(`/api/countries?q=${encodeURIComponent(query)}`, {
signal,
})
return res.json()
}
</script>
<template>
<VPick :options="[]" :fetch-options="fetchOptions" multiple />
</template>Type a few letters. The results come back after a short delay.
[]
- When it asks. Once typing pauses for
searchDebouncemilliseconds (300 by default). The input is never disabled, so the user can keep typing. - Newer typing wins. Starting a new query aborts the previous request through
signal, and a late answer to an older query is never shown. Passingsignalon tofetchis optional, but saves the request. - While waiting. The list shows only
searchingText, so nothing out of date can be picked. - Results are shown as returned, with no filtering on top.
filterdoes not apply here. Each query's answer is kept, so deleting a letter shows the earlier results without asking again. - Nothing typed. The list shows
options, which is a good place for recent or suggested picks. Withoptionsempty it showssearchPromptTextinstead. - Failure. A rejected promise shows
searchErrorText. Clicking it asks again. - Selections outlive the results they came from. A picked option keeps its chip or label, its form value and its
select/deselectpayload when a later search no longer returns it. Put already-selected options inoptionsso they have a label before any search runs.
Searching in the browser. fetchOptions does not have to fetch anything. It can return the results directly instead of a promise, which is how to plug in a search engine that runs in the page, with its own ranking. Set searchDebounce to 0 so the list updates on every keystroke:
<VPick
:options="[]"
:fetch-options="(query) => myEngine.search(query)"
:search-debounce="0"
/>fetchOptions always uses the search input, even without searchable.
Search with multiple
multiple always uses the searchable trigger, because it is the only one that draws chips. Passing :searchable="false" alongside it still renders the input, and logs a warning in development. See Multiselect.
Props
searchableboolean default falseSwaps the button trigger for an input with type-ahead filtering. No effect with multiple, which always uses that trigger.
filter(option, query) => boolean default undefinedReplaces the built-in matcher. Receives each option and the current query.
searchNestedboolean default falseIn tree mode, lets a multi-word query match across a node's ancestor path.
closeOnSelectboolean default see descriptionCloses the list after picking. Defaults to true in single-select and false in multiple; an explicit value applies to both.
searchKeysstring | string[] default undefinedExtra fields on your option objects to search, alongside the label.
fetchOptions(query: string, context: { signal?: AbortSignal }) => unknown[] | Promise<unknown[]> default undefinedAnswers what the user types with options, from a server or an in-browser search engine, instead of filtering options.
searchDebouncenumber default 300Milliseconds to wait after the last keystroke before calling fetchOptions. 0 asks on every keystroke.
searchingTextstring default "Searching..."Shown while fetchOptions is working on the typed query.
searchErrorTextstring default "Could not search. Click to retry"Shown when fetchOptions rejects. Clicking it asks again.
searchPromptTextstring default "Type to search"Shown with fetchOptions when nothing is typed and options is empty.