Hacktron Design System

DropdownMenu

A list of actions or options, triggered by 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
itemsDropdownMenuItem[] | DropdownMenuItem[][]

Slots

Name Description
defaultThe 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.

Usage

Examples

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>

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

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>

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 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>

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>

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>

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>

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>