Component

Dropdown

<DomDropdown>

A menu that opens from a button — fully keyboard accessible, with focus return, optional scroll lock, and outside-click handling.

Playground

Try every prop live

Dropdown playground

Edit props in the inspector — open the menu to preview item labels and alignment.

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

const data = reactive({
	  "items": [
	    {
	      "label": "First option",
	      "value": "a"
	    },
	    {
	      "label": "Second option",
	      "value": "b"
	    },
	    {
	      "label": "Third option",
	      "value": "c"
	    }
	  ],
	  "label": "Choose…",
	  "align": "left",
	  "placement": "bottom",
	  "collisionPadding": 8,
	  "floatingMode": "viewport",
	  "lockScroll": false,
	  "width": "min-w-[12rem]",
	  "panelType": "menu"
	});
</script>

<template>
	<DomDropdown
		v-bind="data"
	/>
</template>

Demo

Basic

Click the trigger to open the menu. Selected value is emitted via @select.

Selected:

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

const items = [
	{ label: 'View profile', value: 'profile' },
	{ label: 'Billing & plans', value: 'billing' },
	{ label: 'Team settings', value: 'team' },
	{ label: 'Sign out', value: 'signout' },
];
const selected = ref('');
</script>

<template>
	<div class="flex flex-col items-center gap-3">
		<DomDropdown :items="items" label="Account ▾" @select="(v) => selected = v" />
		<p class="text-xs text-muted-fg">
			Selected: <code class="text-fg">{{ selected || '—' }}</code>
		</p>
	</div>
</template>

Demo

Custom item markup

The #trigger and #item slots accept any markup — avatars, descriptions, badges.

Rich.vuevue
<script setup>
import { computed, ref } from 'vue';
import { DomDropdown } from '@getdom/studio/vue';

const items = [
	{ value: 'sara', name: 'Sara Patel', role: 'Design', initials: 'SP', color: 'bg-rose-500' },
	{ value: 'tomas', name: 'Tomás Ruiz', role: 'Engineering', initials: 'TR', color: 'bg-success' },
	{ value: 'priya', name: 'Priya Nair', role: 'Product', initials: 'PN', color: 'bg-warning' },
	{ value: 'leo', name: 'Leo Almeida', role: 'Operations', initials: 'LA', color: 'bg-sky-500' },
];
const assigned = ref('sara');
const current = computed(() => items.find((i) => i.value === assigned.value));
</script>

<template>
	<DomDropdown :items="items" width="min-w-[16rem]" @select="(v) => assigned = v">
		<template #trigger>
			<span class="flex items-center gap-2">
				<span
					class="grid size-6 place-items-center rounded-full text-[10px] font-semibold text-white"
					:class="current?.color"
				>{{ current?.initials }}</span>
				<span>{{ current?.name }}</span>
			</span>
		</template>

		<template #item="{ item }">
			<div class="flex items-center gap-3">
				<span
					class="grid size-7 place-items-center rounded-full text-xs font-semibold text-white"
					:class="item.color"
				>{{ item.initials }}</span>
				<div class="min-w-0">
					<p class="truncate text-sm font-medium text-fg">{{ item.name }}</p>
					<p class="truncate text-xs text-muted-fg">{{ item.role }}</p>
				</div>
			</div>
		</template>
	</DomDropdown>
</template>

Demo

Actions menu — icon + label

Items with separator: true draw a divider. tone: 'danger' colours destructive rows red.


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

const icons = {
	edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />',
	duplicate: '<rect x="9" y="9" width="13" height="13" rx="2" ry="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />',
	share: '<circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><line x1="8.59" y1="13.51" x2="15.42" y2="17.49" /><line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />',
	archive: '<polyline points="21 8 21 21 3 21 3 8" /><rect x="1" y="3" width="22" height="5" /><line x1="10" y1="12" x2="14" y2="12" />',
	trash: '<polyline points="3 6 5 6 21 6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /><line x1="10" y1="11" x2="10" y2="17" /><line x1="14" y1="11" x2="14" y2="17" />',
};
const actions = [
	{ label: 'Edit', value: 'edit', icon: 'edit' },
	{ label: 'Duplicate', value: 'duplicate', icon: 'duplicate' },
	{ label: 'Share', value: 'share', icon: 'share' },
	{ label: 'Archive', value: 'archive', icon: 'archive' },
	{ separator: true },
	{ label: 'Delete', value: 'delete', icon: 'trash', tone: 'danger' },
];
const lastAction = ref('');
</script>

<template>
	<div class="flex flex-col items-center gap-3">
		<DomDropdown
			:items="actions"
			label="Actions"
			align="right"
			width="min-w-[14rem]"
			@select="(v) => lastAction = v"
		>
			<template #item="{ item }">
				<span class="flex items-center gap-2.5" :class="item.tone === 'danger' && 'text-destructive'">
					<svg
						viewBox="0 0 24 24"
						class="size-4 opacity-80"
						fill="none"
						stroke="currentColor"
						stroke-width="1.6"
						stroke-linecap="round"
						stroke-linejoin="round"
						v-html="icons[item.icon]"
					/>
					<span>{{ item.label }}</span>
				</span>
			</template>
		</DomDropdown>
		<p v-if="lastAction" class="text-xs text-muted-fg">
			Last action: <code class="text-fg">{{ lastAction }}</code>
		</p>
	</div>
</template>

Demo

Tree view panel

The #panel slot can host composite controls, so the tree keeps its own focus and arrow-key behaviour inside a scroll-locking dropdown.

Move to

Workspace
Overview
Design system
Product
Roadmap
Feedback

Destination: Design system

TreePanel.vuevue
<script setup>
import { computed, ref } from 'vue';
import { DomDropdown, DomTreeView } from '@getdom/studio/vue';

const folderIcon = 'M4 6h6l2 2h8v10H4V6Z';
const screenIcon = 'M5 5h14v10H5V5Zm4 14h6M10 15l-.5 4M14 15l.5 4';
const settingsIcon = 'M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm0-5v3M12 18v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M3 12h3M18 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1';
const fileIcon = 'M6 4h8l4 4v12H6V4Zm8 0v5h5';

const dropdown = ref(null);
const selected = ref('workspace-design-system');
const sections = ref([
	{
		id: 'workspace',
		label: 'Workspace',
		icon: folderIcon,
		open: true,
		children: [
			{ id: 'workspace-overview', label: 'Overview', icon: screenIcon },
			{ id: 'workspace-design-system', label: 'Design system', icon: settingsIcon },
			{ id: 'workspace-release-notes', label: 'Release notes', icon: fileIcon, children: [
				{ id: 'workspace-release-notes-1.0.0', label: '1.0.0', icon: fileIcon },
				{ id: 'workspace-release-notes-1.0.1', label: '1.0.1', icon: fileIcon },
				{ id: 'workspace-release-notes-1.0.2', label: '1.0.2', icon: fileIcon, children: [
					{ id: 'workspace-release-notes-1.0.2-1.0.0', label: '1.0.0', icon: fileIcon },
					{ id: 'workspace-release-notes-1.0.2-1.0.1', label: '1.0.1 best practices', icon: fileIcon, children: [
						{ id: 'workspace-release-notes-1.0.2-1.0.1-1.0.0', label: '1.0.0 The complete guide to Using AI', icon: fileIcon },
					] },
				] },
			] },
		],
	},
	{
		id: 'product',
		label: 'Product',
		icon: folderIcon,
		open: true,
		children: [
			{ id: 'product-roadmap', label: 'Roadmap', icon: fileIcon },
			{ id: 'product-feedback', label: 'Feedback', icon: screenIcon },
		],
	},
	{
		id: 'operations',
		label: 'Operations',
		icon: folderIcon,
		children: [
			{ id: 'operations-hiring', label: 'Hiring', icon: fileIcon },
			{ id: 'operations-finance', label: 'Finance', icon: fileIcon },
		],
	},
]);

function findSection(nodes, value) {
	for (const node of nodes) {
		if (node.id === value) return node;
		const match = node.children ? findSection(node.children, value) : null;
		if (match) return match;
	}
	return null;
}

const selectedSection = computed(() => findSection(sections.value, selected.value));

function chooseSection({ value }) {
	selected.value = value;
	dropdown.value?.close();
}
</script>

<template>
	<div class="flex flex-col items-center gap-3">
		<DomDropdown
			ref="dropdown"
			label="Move to"
			width="w-80"
			panel-type="tree"
			lock-scroll
		>
			<template #trigger>
				<span class="flex min-w-0 items-center gap-2">
					<svg viewBox="0 0 24 24" class="size-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round">
						<path :d="folderIcon" />
					</svg>
					<span class="max-w-40 truncate">{{ selectedSection?.label }}</span>
				</span>
			</template>

			<template #panel>
				<div class="w-full p-1">
					<div class="px-2 pb-2 pt-1">
						<p class="text-xs font-medium uppercase tracking-wide text-muted-fg">Move to</p>
					</div>
					<DomTreeView
						v-model="selected"
						:items="sections"
						:draggable="false"
						:chrome="false"
						density="comfortable"
						variant="finder"
						label="Project sections"
						@select="chooseSection"
					/>
				</div>
			</template>
		</DomDropdown>
		<p class="text-xs text-muted-fg">
			Destination: <code class="text-fg">{{ selectedSection?.label }}</code>
		</p>
	</div>
</template>

Reference

Props

Control props

NameTypeTSDefaultDescription
items
[
	{
		label: "Item 1",
		value: "item-1",
	}
]
arrayArray<ItemsItem
type ItemsItem = {
	label?: string; // Label
	value?: string; // Value
};
>
[]Menu items shown when the dropdown opens.
labelstringstring'Options'Trigger button label (overridden by the #trigger slot if used).
align'left' | 'right'string'left'Where the menu opens relative to the trigger.
placement'bottom' | 'top' | 'right' | 'left'string'bottom'Preferred side before collision handling.
collisionPaddingnumbernumber8Viewport padding used when the menu flips or shifts.
floatingMode'viewport' | 'anchor'string'viewport'viewport keeps the menu inside the browser; anchor keeps it attached while scrolling.
lockScrollbooleanbooleanfalseLock browser scrolling while the dropdown is open.
widthstringstring'min-w-[12rem]'Tailwind width utility for the menu (e.g. min-w-[16rem]).
panelType'menu' | 'tree' | 'dialog' | 'listbox' | 'grid'string'menu'Accessible popup type. Use tree/dialog/etc when rendering the #panel slot.

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

Slots

NameScopeDescription
#triggerReplaces the inner content of the trigger button.
#item{ item, index }Replaces the rendering of each menu item.
#panel{ close, open, toggle }Replaces menu items with custom dropdown content, such as a tree view or form.

Events

NamePayloadDescription
@select(value
value: string;
: string)
Fired when a menu item is chosen.

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

Keyboard

  • Enter / Space / ↓Open menu (when trigger is focused).
  • ↑ / ↓Move active item.
  • Home / EndJump to first / last item.
  • EnterSelect active item.
  • Esc / TabClose menu and return focus.