Component

Select

<DomSelect>

A styled select control with a closed trigger, floating listbox, keyboard navigation, and rich option slots.

Playground

Try every prop live

Select playground

Use DomSelect instead of DomNativeSelect when options need custom rows, metadata, status, or previews.

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

const data = reactive({
	  "modelValue": "pro",
	  "id": "",
	  "name": "",
	  "label": "Plan",
	  "description": "",
	  "placeholder": "Select an option",
	  "required": false,
	  "disabled": false,
	  "readOnly": false,
	  "invalid": false,
	  "errors": {},
	  "visible": true,
	  "validators": [],
	  "validateOnBlur": true,
	  "chrome": "field",
	  "options": [
	    {
	      "label": "Starter",
	      "value": "starter",
	      "description": "For small projects."
	    },
	    {
	      "label": "Pro",
	      "value": "pro",
	      "description": "For growing teams."
	    },
	    {
	      "label": "Enterprise",
	      "value": "enterprise",
	      "description": "For regulated work."
	    }
	  ],
	  "placement": "bottom",
	  "align": "left",
	  "floatingMode": "viewport",
	  "width": "min-w-[14rem]"
	});
</script>

<template>
	<DomSelect
		v-bind="data"
		@update:modelValue="data.modelValue = $event"
	/>
</template>

Demo

Workspace switcher

The value and option slots let a select show avatars, badges, descriptions, and counts while v-model stores a simple value.

Use DomSelect when options need richer markup than a native select can render.

Selected value: growth

<script setup>
import { computed, ref } from 'vue';
import { DomAvatar, DomBadge, DomSelect, DomStatusPill } from '@getdom/studio/vue';

const workspace = ref('growth');
const workspaces = [
	{
		value: 'growth',
		label: 'Growth workspace',
		description: 'Campaigns, experiments, and referral programs.',
		initials: 'GW',
		src: 'https://images.unsplash.com/photo-1527980965255-d3b416303d12?auto=format&fit=crop&crop=faces&w=96&h=96&q=80',
		tone: 'success',
		plan: 'Scale',
		count: '18 members',
	},
	{
		value: 'platform',
		label: 'Platform team',
		description: 'API keys, webhooks, usage limits, and internal tools.',
		initials: 'PT',
		src: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&crop=faces&w=96&h=96&q=80',
		tone: 'info',
		plan: 'Enterprise',
		count: '42 members',
	},
	{
		value: 'support',
		label: 'Support operations',
		description: 'Ticket routing, SLAs, health alerts, and playbooks.',
		initials: 'SO',
		src: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&crop=faces&w=96&h=96&q=80',
		tone: 'warning',
		plan: 'Business',
		count: '11 members',
	},
];
const current = computed(() => workspaces.find((item) => item.value === workspace.value));
</script>

<template>
	<div class="grid w-full max-w-md gap-3">
		<DomSelect
			v-model="workspace"
			label="Workspace"
			description="Use DomSelect when options need richer markup than a native select can render."
			:options="workspaces"
			width="min-w-[22rem]"
		>
			<template #value>
				<span v-if="current" class="flex min-w-0 items-center gap-2">
					<DomAvatar :src="current.src" :name="current.label" :initials="current.initials" size="xs" />
					<span class="truncate">{{ current.label }}</span>
					<DomStatusPill :tone="current.tone" size="sm">{{ current.plan }}</DomStatusPill>
				</span>
			</template>

			<template #option="{ option, selected }">
				<span class="flex items-start gap-3">
					<DomAvatar :src="option.src" :name="option.label" :initials="option.initials" size="sm" />
					<span class="min-w-0 flex-1">
						<span class="flex items-center gap-2">
							<span class="truncate font-medium">{{ option.label }}</span>
							<DomStatusPill :tone="option.tone" size="sm">{{ option.plan }}</DomStatusPill>
						</span>
						<span class="mt-1 block text-xs leading-5 text-muted-fg">{{ option.description }}</span>
						<span class="mt-2 flex items-center gap-2">
							<DomBadge size="sm" tone="neutral">{{ option.count }}</DomBadge>
							<DomBadge v-if="selected" size="sm" tone="success">Selected</DomBadge>
						</span>
					</span>
				</span>
			</template>
		</DomSelect>

		<p class="text-xs text-muted-fg">Selected value: <code class="text-fg">{{ workspace }}</code></p>
	</div>
</template>

Demo

Plan picker

Use a rich select for plan, workspace, role, provider, template, and project pickers instead of a native select.

<script setup>
import { ref } from 'vue';
import { DomBadge, DomSelect } from '@getdom/studio/vue';

const plan = ref('pro');
const plans = [
	{ value: 'starter', label: 'Starter', description: 'For personal tools and prototypes.', price: '$19', badge: 'Good for trials' },
	{ value: 'pro', label: 'Pro', description: 'For small teams shipping customer workflows.', price: '$79', badge: 'Most popular' },
	{ value: 'enterprise', label: 'Enterprise', description: 'For SSO, audit logs, governance, and dedicated support.', price: 'Custom', badge: 'Requires sales' },
];
</script>

<template>
	<div class="w-full max-w-md">
		<DomSelect v-model="plan" label="Plan" :options="plans" width="min-w-[20rem]">
			<template #option="{ option, selected }">
				<span class="flex items-start justify-between gap-4">
					<span class="min-w-0">
						<span class="flex items-center gap-2">
							<span class="font-semibold">{{ option.label }}</span>
							<DomBadge v-if="selected" size="sm" tone="success">Current</DomBadge>
						</span>
						<span class="mt-1 block text-xs leading-5 text-muted-fg">{{ option.description }}</span>
						<DomBadge class="mt-2" size="sm" tone="neutral">{{ option.badge }}</DomBadge>
					</span>
					<span class="shrink-0 text-sm font-semibold text-fg">{{ option.price }}</span>
				</span>
			</template>
		</DomSelect>
	</div>
</template>

Reference

Props

Control props

NameTypeTSDefaultDescription
modelValuestring | numberstring''Selected option value.
placeholderstringstring'Select an option'Text shown when no option is selected.
options*
[
	{
		label: "Option 1",
		value: "option-1",
		description: "Optional helper text",
	}
]
arrayArray<OptionsItem
type OptionsItem = {
	label?: string; // Label
	value?: string; // Value
	description?: string; // Description
};
>
Available options. Use the option slot for rich rows.
placement'bottom' | 'top' | 'right' | 'left'string'bottom'Preferred side before collision handling.
align'left' | 'right'string'left'Horizontal panel alignment.
floatingMode'viewport' | 'anchor'string'viewport'viewport keeps the list inside the browser; anchor keeps it attached while scrolling.
widthstringstring'min-w-[14rem]'Tailwind width utility for the floating listbox.

Field props

NameTypeTSDefaultDescription
idstringstring''Optional ID override. By default parent forms derive the input ID from the field path using underscores.
namestringstring''Local field name. Parent forms derive the full field path and native HTML name from the form hierarchy.
labelstringstring''Visible field label.
descriptionstringstring''Optional helper copy below the field.
requiredbooleanbooleanfalseMark the field as required.
disabledbooleanbooleanfalseDisable field interaction.
readOnlybooleanbooleanfalseShow the value but prevent editing.
invalidbooleanbooleanfalseMark the field invalid.
errors
[
	{
		name: "Validation name",
		message: "Error message",
	}
]
array | object | stringArray<ErrorsItem
type ErrorsItem = {
	name?: string; // Name
	message?: string; // Message
};
>
{}Validation errors for this field.
visiblebooleanbooleantrueShow or hide the field.
validatorsarrayArray<unknown>[]Validators attached to this field. Use functions in Vue code, or serializable records such as { name: "minLength", props: { min: 2 } } in generated schemas.
validateOnBlurbooleanbooleantrueRun validators when the field loses focus.
chrome'field' | falsestring'field'Render default field chrome, or false to render only the control while keeping form state wiring.

Auto-generated from Select.props and inline _edit hints.

Slots

NameScopeDescription
#value{ option, value, label, placeholder }Custom selected-value markup inside the trigger.
#option{ option, index, selected }Custom option markup inside the floating list.

Events

NamePayloadDescription
@update:modelValue(value
value: string;
: string)
Emitted when the selected option changes.
@select({ option, value, label })Emitted with the selected option record.
@focus
@blur

Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.

Keyboard

  • Enter / Space / ↓Open the listbox.
  • ↑ / ↓Move active option.
  • Enter / SpaceSelect active option.
  • Esc / TabClose and return focus.