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.
<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
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
modelValue | string | number | string | '' | Selected option value. |
placeholder | string | string | 'Select an option' | Text shown when no option is selected. |
options* | array | Array< | — | 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. |
width | string | string | 'min-w-[14rem]' | Tailwind width utility for the floating listbox. |
Field props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
id | string | string | '' | Optional ID override. By default parent forms derive the input ID from the field path using underscores. |
name | string | string | '' | Local field name. Parent forms derive the full field path and native HTML name from the form hierarchy. |
label | string | string | '' | Visible field label. |
description | string | string | '' | Optional helper copy below the field. |
required | boolean | boolean | false | Mark the field as required. |
disabled | boolean | boolean | false | Disable field interaction. |
readOnly | boolean | boolean | false | Show the value but prevent editing. |
invalid | boolean | boolean | false | Mark the field invalid. |
errors | array | object | string | Array< | {} | Validation errors for this field. |
visible | boolean | boolean | true | Show or hide the field. |
validators | array | Array<unknown> | [] | Validators attached to this field. Use functions in Vue code, or serializable records such as { name: "minLength", props: { min: 2 } } in generated schemas. |
validateOnBlur | boolean | boolean | true | Run validators when the field loses focus. |
chrome | 'field' | false | string | '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
| Name | Scope | Description |
|---|---|---|
| #value | { option, value, label, placeholder } | Custom selected-value markup inside the trigger. |
| #option | { option, index, selected } | Custom option markup inside the floating list. |
Events
| Name | Payload | Description |
|---|---|---|
| @update:modelValue | ( | 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.