Component

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

PeoplePlayground.vuevue
<script setup>
import { reactive } from 'vue';
import { DomCombobox } from '@getdom/studio/vue';

const data = reactive({
	modelValue: 'ada',
	options: [
		{ value: 'ada', label: 'Ada Lovelace', role: 'Mathematician', avatar: 'https://unavatar.io/github/adafruit' },
		{ value: 'grace', label: 'Grace Hopper', role: 'Computer scientist', avatar: 'https://unavatar.io/github/ghopper' },
		{ value: 'katherine', label: 'Katherine Johnson', role: 'NASA mathematician', avatar: 'https://unavatar.io/github/nasa' },
	],
	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">
					<img :src="item.avatar" alt="" class="size-8 rounded-full bg-secondary object-cover" />
					<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-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:

PickFruit.vuevue
<script setup>
import { DomCombobox } from '@getdom/studio/vue';
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-fg">{{ value || '—' }}</code></p>
	</div>
</template>

Demo

Custom item markup

Use the item slot for richer option rendering. The input still displays the label and v-model still receives the value.

  • Ada LovelaceMathematician
  • Grace HopperComputer scientist
  • Katherine JohnsonNASA mathematician

Selected value:

PeopleLookup.vuevue
<script setup>
import { DomCombobox } from '@getdom/studio/vue';
import { ref } from 'vue';

const value = ref('');
const people = [
	{ value: 'ada', label: 'Ada Lovelace', role: 'Mathematician', avatar: 'https://unavatar.io/github/adafruit' },
	{ value: 'grace', label: 'Grace Hopper', role: 'Computer scientist', avatar: 'https://unavatar.io/github/ghopper' },
	{ value: 'katherine', label: 'Katherine Johnson', role: 'NASA mathematician', avatar: 'https://unavatar.io/github/nasa' },
];
</script>

<template>
	<div class="flex w-full max-w-md flex-col gap-2">
		<DomCombobox v-model="value" :options="people" placeholder="Assign a person" class="w-full">
			<template #item="{ item }">
				<div class="flex items-center gap-3">
					<img :src="item.avatar" alt="" class="size-8 rounded-full bg-secondary object-cover" />
					<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-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.

Options are fetched by the parent in response to @query.

Selected value: None

ServerLoadedPeople.vuevue
<script setup>
import { ref } from 'vue';
import { DomCombobox } from '@getdom/studio/vue';
import { searchPeople } from '../../_shared/serverLookup.js';

const value = ref('');
const options = ref([]);
const loading = ref(false);
let requestId = 0;

async function queryPeople(query) {
	const currentRequest = ++requestId;
	if (!query.trim()) {
		options.value = [];
		loading.value = false;
		return;
	}

	loading.value = true;
	const results = await searchPeople(query);
	if (currentRequest !== requestId) return;
	options.value = results;
	loading.value = false;
}
</script>

<template>
	<div class="grid w-full max-w-md gap-3">
		<DomCombobox
			v-model="value"
			:options="options"
			:loading="loading"
			label="Assign owner"
			description="Options are fetched by the parent in response to @query."
			placeholder="Type a name"
			class="w-full"
			@query="queryPeople"
		>
			<template #item="{ item }">
				<span class="block min-w-0">
					<span class="block truncate font-medium">{{ item.label }}</span>
					<span class="block truncate text-xs text-muted-fg">{{ item.email }} - {{ item.role }}</span>
				</span>
			</template>
		</DomCombobox>
		<p class="rounded-2xl border border-border bg-secondary/40 p-3 text-xs text-muted-fg">
			Selected value: <code class="font-mono text-fg">{{ 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

Props

Props

NameTypeDefaultDescription
v-modelstringSelected 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.

Keyboard

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