DropdownMenu
A list of actions that opens from a button.
<template> <DropdownMenu :items="[[{ label: 'Edit', icon: 'i-lucide-pencil', onSelect: onEdit }], [{ label: 'Delete', icon: 'i-lucide-trash', color: 'error', onSelect: onDelete }]]"> <Button variant="secondary" trailing-icon="i-lucide-chevron-down">Actions</Button> </DropdownMenu></template>Props
| Name | Type | Default |
|---|---|---|
| items | DropdownMenuItem[] | DropdownMenuItem[][] | - |
Slots
| Name | Description |
|---|---|
| default | The trigger. Any element works, but real usage is always a Button - a labeled one with a trailing chevron for most menus, or an icon-only kebab for an overflow menu. |
Guidelines
Do
- Use a labeled trigger with a trailing chevron (
trailing-icon="i-lucide-chevron-down") for most menus - the pattern nearly every real usage inapps/frontendfollows. - Use an icon-only trigger (e.g.
icon="i-lucide-more-horizontal", no label) for a kebab/overflow menu, and pass:content="{ align: 'end' }"so it doesn't overflow past the trigger's right edge. - Group related items with a 2D array (
DropdownMenuItem[][]) - an automatic divider is inserted between groups, no separatetype: 'separator'entry needed.
- Use
childrenon an item for a submenu, andonSelecton each item for its action - both match the real user menu's own org-switching pattern. - Use a
secondaryButton for a filter-style action trigger, and aplainone at the row's size when the trigger sits among small plain actions. A control that picks a value (a date range, a "Group by") is aSelect, not a menu. - Set
color: 'error'on a destructive item (remove, revoke, delete) - it stays red at rest and on hover. Leave its icon out, and the icon of an exit item (log out, leave organization), so it stands apart from the iconned items above it. A menu can mix items with and without icons. - For custom content in some rows only, give those items
slot: 'name'and fill#name-leading- the org switcher renders each org'sAvatarthis way.
Don't
- Don't use a menu to set a field's value (a finding's status or severity): use
Select, orComboboxwhen the list needs search. A menu is for actions. - Don't reach for
type: 'checkbox'or keyboard shortcuts (kbds) unless the design genuinely needs them - no real usage has either yet, and both add interaction complexity most menus don't call for. - Don't forget alignment when the trigger sits near a container's edge - the default alignment can otherwise push the menu outside the viewport.
- Don't pass
:ui- it would replace the whole design-system look (panel and option rows), so the type-check rejects it. Every menu and picker shares one option-row style. The trigger is your own slot content, so style it as aButton.
Examples
Variants
Basic
A grouped menu with a section label and a separator between groups.
const items = [ [ { type: 'label', label: 'My Account' }, { label: 'Profile', icon: 'i-lucide-user' }, { label: 'Billing', icon: 'i-lucide-credit-card' }, ], [ { label: 'Team', icon: 'i-lucide-users' }, { label: 'Subscription', icon: 'i-lucide-badge-dollar-sign' }, ],]<DropdownMenu :items="items"> <Button variant="secondary" trailing-icon="i-lucide-chevron-down">Open</Button></DropdownMenu>Submenu
A nested menu opened from a parent item.
const items = [ { label: 'Profile', icon: 'i-lucide-user' }, { label: 'Invite users', icon: 'i-lucide-user-plus', children: [ { label: 'Email', icon: 'i-lucide-mail' }, { label: 'Message', icon: 'i-lucide-message-square' }, { type: 'separator' }, { label: 'More...', icon: 'i-lucide-plus-circle' }, ], }, { label: 'Log out', icon: 'i-lucide-log-out' },]<DropdownMenu :items="items"> <Button variant="secondary" trailing-icon="i-lucide-chevron-down">Open</Button></DropdownMenu>Checkboxes
Independent toggles that stay open after each click.
const showStatusBar = ref(true)const showActivityBar = ref(false)const items = computed(() => [ { type: 'label', label: 'Appearance' }, { type: 'checkbox', label: 'Status Bar', checked: showStatusBar.value, onUpdateChecked: (c) => showStatusBar.value = c }, { type: 'checkbox', label: 'Activity Bar', checked: showActivityBar.value, onUpdateChecked: (c) => showActivityBar.value = c }, { type: 'checkbox', label: 'Panel', disabled: true },])<DropdownMenu :items="items"> <Button variant="secondary" trailing-icon="i-lucide-chevron-down">View</Button></DropdownMenu>Radio Group
Mutually exclusive options, only one selected at a time.
const paymentMethod = ref('card')// UDropdownMenu has no native radio item type - type: 'checkbox' items that all write to// the same ref, keyed to their own value, approximate one instead.const items = computed(() => [ { type: 'label', label: 'Payment Method' }, { type: 'checkbox', label: 'Credit Card', checked: paymentMethod.value === 'card', onUpdateChecked: () => paymentMethod.value = 'card' }, { type: 'checkbox', label: 'PayPal', checked: paymentMethod.value === 'paypal', onUpdateChecked: () => paymentMethod.value = 'paypal' }, { type: 'checkbox', label: 'Apple Pay', checked: paymentMethod.value === 'apple-pay', onUpdateChecked: () => paymentMethod.value = 'apple-pay' },])<DropdownMenu :items="items"> <Button variant="secondary" trailing-icon="i-lucide-chevron-down">Payment Method</Button></DropdownMenu>Destructive
A destructive action set apart from the rest of the menu.
const items = [ [ { type: 'label', label: 'Actions' }, { label: 'Edit', icon: 'i-lucide-pencil' }, { label: 'Duplicate', icon: 'i-lucide-copy' }, ], [ { label: 'Delete', icon: 'i-lucide-trash', color: 'error' }, ],]<DropdownMenu :items="items"> <Button variant="secondary" trailing-icon="i-lucide-chevron-down">Actions</Button></DropdownMenu>With icons and slots
Shortcuts
Keyboard shortcut hints shown next to each item.
const items = [ { label: 'Profile', kbds: ['meta', 'P'] }, { label: 'Billing', kbds: ['meta', 'B'] }, { label: 'Settings', kbds: ['meta', 'S'] }, { label: 'Keyboard shortcuts', kbds: ['meta', 'K'] },]<DropdownMenu :items="items"> <Button variant="secondary" trailing-icon="i-lucide-chevron-down">Open</Button></DropdownMenu>Icons
A leading icon on every item.
const items = [ { label: 'Profile', icon: 'i-lucide-user' }, { label: 'Billing', icon: 'i-lucide-credit-card' }, { label: 'Settings', icon: 'i-lucide-settings' }, { label: 'Log out', icon: 'i-lucide-log-out' },]<DropdownMenu :items="items"> <Button variant="secondary" trailing-icon="i-lucide-chevron-down">Open</Button></DropdownMenu>Checkboxes Icons
Checkbox items whose icon reflects their own checked state.
const notifyPush = ref(true)const notifyEmail = ref(false)const items = computed(() => [ { type: 'label', label: 'Notifications' }, { type: 'checkbox', label: 'Push notifications', icon: notifyPush.value ? 'i-lucide-bell' : 'i-lucide-bell-off', checked: notifyPush.value, onUpdateChecked: (c) => notifyPush.value = c }, { type: 'checkbox', label: 'Email notifications', icon: notifyEmail.value ? 'i-lucide-mail' : 'i-lucide-mail-x', checked: notifyEmail.value, onUpdateChecked: (c) => notifyEmail.value = c },])<DropdownMenu :items="items"> <Button variant="secondary" trailing-icon="i-lucide-chevron-down">Notifications</Button></DropdownMenu>Radio Icons
A radio group with a leading icon per option.
const radioMethod = ref('card')const items = computed(() => [ { type: 'label', label: 'Payment Method' }, { type: 'checkbox', label: 'Credit Card', icon: 'i-lucide-credit-card', checked: radioMethod.value === 'card', onUpdateChecked: () => radioMethod.value = 'card' }, { type: 'checkbox', label: 'PayPal', icon: 'i-lucide-wallet', checked: radioMethod.value === 'paypal', onUpdateChecked: () => radioMethod.value = 'paypal' }, { type: 'checkbox', label: 'Apple Pay', icon: 'i-lucide-smartphone', checked: radioMethod.value === 'wallet', onUpdateChecked: () => radioMethod.value = 'wallet' },])<DropdownMenu :items="items"> <Button variant="secondary" trailing-icon="i-lucide-chevron-down">Payment Method</Button></DropdownMenu>Avatar
A user menu opened from an avatar instead of a button.
const items = [ [ { type: 'label', label: 'Leo Rivera' }, { label: 'Profile', icon: 'i-lucide-user' }, { label: 'Settings', icon: 'i-lucide-settings' }, ], [ { label: 'Log out', icon: 'i-lucide-log-out' }, ],]<DropdownMenu :items="items" :content="{ align: 'end' }"> <Avatar alt="Leo Rivera" size="sm" /></DropdownMenu>Composition
Complex
Shortcuts, icons, and a submenu combined in one real-world menu.
const items = [ [ { type: 'label', label: 'My Account' }, { label: 'Profile', icon: 'i-lucide-user', kbds: ['meta', 'P'] }, { label: 'Billing', icon: 'i-lucide-credit-card', kbds: ['meta', 'B'] }, { label: 'Invite users', icon: 'i-lucide-user-plus', children: [ { label: 'Email', icon: 'i-lucide-mail' }, { label: 'Message', icon: 'i-lucide-message-square' }, ], }, ], [ { label: 'Log out', icon: 'i-lucide-log-out', kbds: ['shift', 'meta', 'Q'] }, ],]<DropdownMenu :items="items"> <Button variant="secondary" trailing-icon="i-lucide-chevron-down">Open</Button></DropdownMenu>