Dropdown Menu
npx @vyui/cli add dropdown-menuOverview
VyDropdownMenu renders a list of actions in an overlay docked to its trigger. It composes the @vyui/core dropdown primitives, takes its rows from an items array, and measures the trigger on open so the menu appears beside it rather than centered on screen.
Usage
The default slot is the trigger. It is already wired to toggle the menu, so do not add a @tap handler that also sets the open state.
<script setup lang="ts">
import { VyDropdownMenu } from '@vyui/kit/dropdown-menu'
import { VyButton } from '@vyui/kit/button'
const items = [
{ label: 'Edit', icon: 'i-lucide-pencil', onSelect: () => edit() },
{ label: 'Duplicate', icon: 'i-lucide-copy', onSelect: () => duplicate() },
{ label: 'Delete', icon: 'i-lucide-trash-2', color: 'error', onSelect: () => remove() },
]
</script>
<template>
<VyDropdownMenu :items="items">
<VyButton label="Actions" trailing-icon="i-lucide-chevron-down" />
</VyDropdownMenu>
</template>
Groups
A nested array renders each inner array as a group with a separator between them. Inside a group, type: 'separator' and type: 'label' add structural rows.
const items = [
[
{ type: 'label', label: 'Document' },
{ label: 'Rename', icon: 'i-lucide-pencil' },
{ label: 'Share', icon: 'i-lucide-share-2' },
],
[
{ label: 'Delete', icon: 'i-lucide-trash-2', color: 'error' },
],
]
Checkbox items
type: 'checkbox' renders a checked indicator and reports changes through onUpdateChecked.
const items = [
{ type: 'checkbox', label: 'Show archived', checked: showArchived, onUpdateChecked: (v: boolean) => (showArchived = v) },
]
Positioning
content chooses the dock edge and offsets, defaulting to { side: 'bottom', sideOffset: 8, align: 'start' }.
<template>
<VyDropdownMenu :items="items" :content="{ side: 'top', align: 'end', sideOffset: 12 }">
<VyButton icon="i-lucide-ellipsis-vertical" />
</VyDropdownMenu>
</template>
Custom rows
#item, #item-leading, #item-label, #item-description, and #item-trailing replace that part of every row. Give a single item a slot key to target it alone through #{slot}-trailing and friends.
<template>
<VyDropdownMenu :items="items">
<VyButton label="Account" />
<template #item-trailing="{ item }">
<text v-if="item.shortcut" class="text-xs text-muted">{{ item.shortcut }}</text>
</template>
</VyDropdownMenu>
</template>
Features and behavior
- The trigger toggles the menu. Bind
openwithv-model:openonly when the state also drives something else. modal(defaulttrue) blocks taps outside the menu from reaching the app; the backdrop still closes the menu.onSelectfires per item;disableditems ignore taps and dim, andloadingspins the leading icon.typedefaults to'link'.'label'and'separator'are structural and not selectable.- Items and groups are flattened into one row list before rendering, so every row emits exactly one node — which is what the Vue-Lynx patcher expects.
labelKeyanddescriptionKeyread the label and sub-line from a different field when items come from an API.checkedIconandloadingIconfall back toappConfig.ui.icons.checkandappConfig.ui.icons.loading.- Submenus are not part of the kit wrapper; compose the core
DropdownMenuSubprimitives directly if you need them.
API
Props
| Prop | Default | Type |
|---|---|---|
checkedIcon | — | string | undefinedIconify name for checked checkbox items. Defaults to `appConfig.ui.icons.check`. |
content | { side: "bottom", sideOffset: 8, align: "start" } | DropdownMenuContentSettings | undefinedPositioning settings — `side`, `align`, `sideOffset` control where the menu docks relative to its trigger. |
defaultOpen | false | boolean | undefinedInitial open state when uncontrolled. |
descriptionKey | "description" | string | undefinedKey on each item used as the rendered description. |
disabled | — | boolean | undefinedDisable the trigger. |
items | — | DropdownMenuItem[] | DropdownMenuItem[][] | undefinedFlat list, OR a nested array where each inner array becomes a group separated by a `<DropdownMenuSeparator>`. |
labelKey | "label" | string | undefinedKey on each item used as the rendered label. |
loadingIcon | — | string | undefinedIconify name for the loading spinner. Defaults to `appConfig.ui.icons.loading`. |
modal | true | boolean | undefinedModality. When `true`, taps outside the menu are blocked from reaching the underlying app. |
open | — | boolean | undefinedControlled open state. |
size | — | "md" | "sm" | "lg" | "xl" | undefined |
ui | — | Partial<Record<"item" | "content" | "label" | "group" | "separator" | "itemLeadingIcon" | "itemLeadingAvatar" | "itemTrailing" | "itemLabel" | "itemLeadingAvatarSize" | "itemWrapper" | "itemDescription", ClassNameValue>> | undefined |
Emits
| Event | Payload |
|---|---|
update:open | [value: boolean] |
Slots
| Slot | Bindings |
|---|---|
default | { open: boolean; }Trigger content. `DropdownMenuTrigger` toggles open state on tap; do NOT also bind a `@tap` handler that sets the open state. |
item | DropdownMenuItemSlotPropsCustom rendering for every item (replaces all per-section defaults). |
item-leading | DropdownMenuItemSlotPropsCustom rendering for every item's leading slot. |
item-label | DropdownMenuItemSlotPropsCustom rendering for every item's label. |
item-description | DropdownMenuItemSlotPropsCustom rendering for every item's description. |
item-trailing | DropdownMenuItemSlotPropsCustom rendering for every item's trailing slot. |
Styling and theming
Override globally through appConfig.ui.dropdownMenu or per instance with ui.
| UI slot | Purpose |
|---|---|
content | Menu panel surface, border, elevation, and scrolling. |
group | Padding around a group of rows. |
label | Non-interactive group heading. |
separator | Divider between rows or groups. |
item | Row layout, radius, and disabled state. |
itemWrapper | Column holding the label and description. |
itemLabel / itemDescription | Row text. |
itemLeadingIcon / itemLeadingAvatar | Leading icon or avatar. |
itemTrailing | Trailing content, including the checked indicator. |
size scales row padding, text, and the leading icon from sm through xl. color on an item colors its label and leading icon; the row surface stays neutral.
Accessibility
Rows come from the core menu primitives, which expose menu-item semantics along with disabled and checked state. Give an icon-only trigger an accessibility-label, and keep a text label on every row — a custom #item slot that renders only an icon leaves the row unannounced.
Platform notes
- The menu is portaled into the overlay root and docked by measuring the trigger with
useElementRecton open and on@layoutchange, then aligning the overlay container with flex alignment and padding. - The first frame after open has no measurement yet, so the panel is rendered transparent until the trigger rect arrives.
- Lynx rasterizes each SVG, so row icons are given a baked hex fill rather than inheriting a text color.