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.
<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: —
<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.
<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.
<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.
Destination: Design system
<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
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
items | array | Array< | [] | Menu items shown when the dropdown opens. |
label | string | string | '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. |
collisionPadding | number | number | 8 | Viewport 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. |
lockScroll | boolean | boolean | false | Lock browser scrolling while the dropdown is open. |
width | string | string | '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
| Name | Scope | Description |
|---|---|---|
| #trigger | — | Replaces 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
| Name | Payload | Description |
|---|---|---|
| @select | ( | 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.