Component
Vue Searchable Select and Combobox
<dom-combobox>A styled select-like control: show the option label, store the option value, and keep keyboard navigation.
Playground
Custom item playground
People combobox playground
Edit people rows in the inspector and the custom item slot keeps rendering the richer markup.
Ada LovelaceMathematician
Grace HopperComputer scientist
Katherine JohnsonNASA mathematician
Selected value: ada
Properties
npm install @getdom/studio<script setup>
import '@getdom/studio/style.css';
import { reactive } from 'vue';
import { DomAvatar, DomCombobox } from '@getdom/studio';
const data = reactive({
modelValue: 'ada',
options: [
{ value: 'ada', label: 'Ada Lovelace', role: 'Mathematician', initials: 'AL', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&crop=faces&w=96&h=96&q=80' },
{ value: 'grace', label: 'Grace Hopper', role: 'Computer scientist', initials: 'GH', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&crop=faces&w=96&h=96&q=80' },
{ value: 'katherine', label: 'Katherine Johnson', role: 'NASA mathematician', initials: 'KJ', avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&crop=faces&w=96&h=96&q=80' },
],
placeholder: 'Assign a person',
placement: 'bottom',
floatingMode: 'viewport',
clearable: true,
loading: false,
});
defineExpose({ data });
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-2">
<DomCombobox
v-model="data.modelValue"
:options="data.options"
:placeholder="data.placeholder"
:placement="data.placement"
:floating-mode="data.floatingMode"
:clearable="data.clearable"
:loading="data.loading"
class="w-full"
>
<template #item="{ item }">
<div class="flex min-w-0 items-center gap-3">
<DomAvatar :src="item.avatar" :name="item.label" :initials="item.initials" size="sm" />
<span class="min-w-0">
<span class="block truncate font-medium">{{ item.label }}</span>
<span class="block truncate text-xs text-muted-fg">{{ item.role }}</span>
</span>
</div>
</template>
</DomCombobox>
<p class="text-xs text-muted-fg">Selected value: <code class="text-canvas-fg">{{ data.modelValue || '—' }}</code></p>
</div>
</template>
Demo
Vue
The input displays the label, while v-model receives the option value.
- Apple
- Banana
- Cherry
Selected value: —
npm install @getdom/studio<script setup>
import '@getdom/studio/style.css';
import { DomCombobox } from '@getdom/studio';
import { ref } from 'vue';
const value = ref('');
const options = [
{ value: '1', label: 'Apple' },
{ value: '2', label: 'Banana' },
{ value: '3', label: 'Cherry' },
];
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-2">
<DomCombobox v-model="value" :options="options" placeholder="Pick a fruit" class="w-full" />
<p class="text-xs text-muted-fg">Selected value: <code class="text-canvas-fg">{{ value || '—' }}</code></p>
</div>
</template>
Demo
Custom item markup
Type ob to highlight Steve Obrien and Robin. The item slot uses query and escaped text segments inside mark elements; literal <Ops> text never becomes HTML.
- Steve ObrienProduct designer
- Robin <Ops>Literal markup remains plain text
Ada LovelaceMathematician
Grace HopperComputer scientist
Katherine JohnsonNASA mathematician
Selected value: —
npm install @getdom/studio<script setup>
import '@getdom/studio/style.css';
import { DomAvatar, DomCombobox } from '@getdom/studio';
import { ref } from 'vue';
import { highlightMatches } from '../../_shared/highlightMatches.js';
const value = ref('');
const people = [
{ value: 'steve', label: 'Steve Obrien', role: 'Product designer', initials: 'SO' },
{ value: 'robin', label: 'Robin <Ops>', role: 'Literal markup remains plain text', initials: 'RO' },
{ value: 'ada', label: 'Ada Lovelace', role: 'Mathematician', initials: 'AL', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&crop=faces&w=96&h=96&q=80' },
{ value: 'grace', label: 'Grace Hopper', role: 'Computer scientist', initials: 'GH', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&crop=faces&w=96&h=96&q=80' },
{ value: 'katherine', label: 'Katherine Johnson', role: 'NASA mathematician', initials: 'KJ', avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&crop=faces&w=96&h=96&q=80' },
];
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-2">
<DomCombobox v-model="value" :options="people" label="Highlighted people" placeholder="Try typing ob or <Ops>" class="w-full">
<template #item="{ item, query }">
<div class="flex items-center gap-3">
<DomAvatar :src="item.avatar" :name="item.label" :initials="item.initials" size="sm" />
<span class="min-w-0">
<span class="block truncate font-medium"><template v-for="(part, index) in highlightMatches(item.label, query)" :key="index"><mark v-if="part.matched" class="rounded-sm bg-primary/20 text-inherit">{{ part.text }}</mark><template v-else>{{ part.text }}</template></template></span>
<span class="block truncate text-xs text-muted-fg">{{ item.role }}</span>
</span>
</div>
</template>
</DomCombobox>
<p class="text-xs text-muted-fg">Selected value: <code class="text-canvas-fg">{{ value || '—' }}</code></p>
</div>
</template>
Demo
Server-loaded options
@query lets the parent fetch options and pass the current server results back through options.
- No options found
A real HTTP endpoint returns 12 of 602 synthetic contacts at a time.
0 of 0 results loaded. Scroll for more; search resets the pages.
Value: None
npm install @getdom/studio<script setup>
import '@getdom/studio/style.css';
import { ref } from 'vue';
import { DomCombobox } from '@getdom/studio';
import { usePagedLookup } from '../../_shared/usePagedLookup.js';
import LookupStatus from '../../_shared/LookupStatus.vue';
const value = ref('');
const { options, loading, hasMore, error, total, search, loadMore } = usePagedLookup('people');
</script>
<template>
<div class="grid w-full max-w-md gap-3">
<DomCombobox
v-model="value"
:options="options"
:loading="loading"
:has-more="hasMore"
:filter-options="false"
label="Assign owner"
description="A real HTTP endpoint returns 12 of 602 synthetic contacts at a time."
placeholder="Search or browse people"
@query="search"
@load-more="loadMore"
>
<template #item="{ item }">
<span class="block truncate font-medium">{{ item.label }}</span>
<span class="block truncate text-xs text-muted-fg">{{ item.email }} · {{ item.role }}</span>
</template>
</DomCombobox>
<LookupStatus :error="error" :count="options.length" :total="total" @retry="loadMore" />
<p class="text-xs text-muted-fg">Value: <code>{{ value || 'None' }}</code></p>
</div>
</template>
Usage
Plain HTML
Use the headless custom element when you want the same combobox behaviour in plain HTML or another framework. The headless page includes copyable DOM-defined list examples.
View headless comboboxBehaviour
Selection, search and remote pages
A selected value is not a search filter. Opening shows the loaded options and a checkmark on matching selections. Only typing filters; custom item/option slots receive query and selected.
Set :filter-options="false" for server-ranked results. Handle @query to reset the cursor and replace options, then @load-more to append the next page. Pass loading and hasMore. Scrolling, keyboard navigation at the last row, and the Load more button request another page.
The examples call /api/form-options, a real HTTP endpoint serving synthetic records. The shared example loader debounces queries, aborts old requests, ignores stale responses, deduplicates pages and exposes a Retry button. Applications own their endpoint, authorization and cursor protocol; the controls do not fetch data themselves.
Selected labels are retained when a page is replaced. For Select, Combobox and Tag combobox, selectedOptions can supply initial records that are not loaded yet. Autocomplete stores the label as free text. Select opens near the selected row when it is loaded; a remote selection on a later page requires an application-owned lookup or seek endpoint. Multi-select opens at the beginning.
Props
Props
| Name | Type | Default | Description |
|---|---|---|---|
| v-model | string | — | Selected option value. |
| options | Array<{ value, label }> | string[] | — | Available options. |
| placeholder | string | 'Search…' | Input placeholder. |
| floatingMode | 'viewport' | 'anchor' | 'viewport' | Choose whether the list stays in the browser or follows the input while scrolling. |
| clearable | boolean | true | Show an inline clear button when an option is selected. |
| loading | boolean | false | Show an inline spinner while async options are being fetched. |
| filterOptions | boolean | true | Set false for server-filtered or server-ranked options. |
| hasMore | boolean | false | Enable incremental scrolling, keyboard paging and the Load more button through @load-more. |
| selectedOptions | DomOption[] | [] | Resolved initial selected records outside the currently loaded page. |
Keyboard
- ↑ / ↓Move active option.
- EnterCommit active option.
- EscClose the list.
- TypeFilter the list live.