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.

Selected value: ada

PeoplePlayground.vuevue
Install
npm install @getdom/studio
vue
<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.

Selected value: —

Install
npm install @getdom/studio
vue
<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.

Selected value: —

Install
npm install @getdom/studio
vue
<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.

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

Install
npm install @getdom/studio
vue
<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 combobox

Behaviour

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

NameTypeDefaultDescription
v-modelstring—Selected option value.
optionsArray<{ value, label }> | string[]—Available options.
placeholderstring'Search…'Input placeholder.
floatingMode'viewport' | 'anchor''viewport'Choose whether the list stays in the browser or follows the input while scrolling.
clearablebooleantrueShow an inline clear button when an option is selected.
loadingbooleanfalseShow an inline spinner while async options are being fetched.
filterOptionsbooleantrueSet false for server-filtered or server-ranked options.
hasMorebooleanfalseEnable incremental scrolling, keyboard paging and the Load more button through @load-more.
selectedOptionsDomOption[][]Resolved initial selected records outside the currently loaded page.

Keyboard

  • ↑ / ↓Move active option.
  • EnterCommit active option.
  • EscClose the list.
  • TypeFilter the list live.