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
vue
<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]",
	  "optionTone": "accent",
	  "searchable": false,
	  "searchPlaceholder": "Search options..."
	});
</script>

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

Demo

Workspace switcher

The searchable panel opens to the full option list, then filters rich rows as the user types while v-model stores a simple value.

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

Selected value: growth

vue
<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',
	},
	{
		value: 'design',
		label: 'Design systems',
		description: 'Component governance, accessibility review, and release notes.',
		initials: 'DS',
		src: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&crop=faces&w=96&h=96&q=80',
		tone: 'neutral',
		plan: 'Team',
		count: '9 members',
	},
	{
		value: 'research',
		label: 'Research lab',
		description: 'Interview notes, insight tagging, and participant panels.',
		initials: 'RL',
		src: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&crop=faces&w=96&h=96&q=80',
		tone: 'info',
		plan: 'Pro',
		count: '6 members',
	},
	{
		value: 'ops',
		label: 'Operations hub',
		description: 'Vendor reviews, procurement workflows, and launch checklists.',
		initials: 'OH',
		src: 'https://images.unsplash.com/photo-1544723795-3fb6469f5b39?auto=format&fit=crop&crop=faces&w=96&h=96&q=80',
		tone: 'warning',
		plan: 'Scale',
		count: '24 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"
			searchable
			search-placeholder="Search 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-canvas-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.

vue
<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-canvas-fg">{{ option.price }}</span>
				</span>
			</template>
		</DomSelect>
	</div>
</template>

Demo

Optional value

Include an option with value '' when the field should support an explicit unset state.

Selected value: (empty)

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

const assignee = ref('');
const people = [
	{ value: '', label: 'Not assigned', description: 'Leave this workflow unowned.' },
	{ value: 'ada', label: 'Ada Lovelace', description: 'Product analytics' },
	{ value: 'grace', label: 'Grace Hopper', description: 'Platform engineering' },
	{ value: 'katherine', label: 'Katherine Johnson', description: 'Launch readiness' },
];
</script>

<template>
	<div class="grid w-full max-w-sm gap-3">
		<DomSelect
			v-model="assignee"
			label="Assignee"
			placeholder="Choose a person"
			:options="people"
			width="min-w-[18rem]"
		>
			<template #option="{ option, selected }">
				<span class="flex items-start justify-between gap-3">
					<span class="min-w-0">
						<span class="font-medium">{{ option.label }}</span>
						<span class="mt-1 block text-xs leading-5 text-muted-fg">{{ option.description }}</span>
					</span>
					<DomStatusPill v-if="selected" size="sm" tone="success">Selected</DomStatusPill>
				</span>
			</template>
		</DomSelect>

		<p class="text-xs text-muted-fg">Selected value: <code class="text-canvas-fg">{{ assignee || '(empty)' }}</code></p>
	</div>
</template>

Reference

Props

Control props

NameTypeTSDefaultDescription
modelValuestring | numberstring''Selected option value.
placeholderstringstring'Select an option'Text shown when no option is selected.
options*
ts
[
	{
		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.
optionTonestringstring'accent'Theme token basename for hovered, focused, and selected options. For example, primary uses var(--primary) and var(--primary-fg).
searchableboolean | stringstringfalseShow a search input inside the option panel while keeping select-style committed values.
searchPlaceholderstringstring'Search options...'Placeholder shown inside the panel search input.

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
ts
[
	{
		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' | 'none' | falsestring'field'Render default field chrome, or hide chrome 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.
  • Type in searchFilter visible options when searchable is enabled.
  • Enter / SpaceSelect active option.
  • Esc / TabClose and return focus.