Components

Dropdown Menu

Source
A menu of actions anchored to the element that opens it.
Install with the CLI
npx @vyui/cli add dropdown-menu

Overview

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 open with v-model:open only when the state also drives something else.
  • modal (default true) blocks taps outside the menu from reaching the app; the backdrop still closes the menu.
  • onSelect fires per item; disabled items ignore taps and dim, and loading spins the leading icon.
  • type defaults 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.
  • labelKey and descriptionKey read the label and sub-line from a different field when items come from an API.
  • checkedIcon and loadingIcon fall back to appConfig.ui.icons.check and appConfig.ui.icons.loading.
  • Submenus are not part of the kit wrapper; compose the core DropdownMenuSub primitives directly if you need them.

API

Props

PropDefaultType
checkedIconstring | undefined

Iconify name for checked checkbox items. Defaults to `appConfig.ui.icons.check`.

content{ side: "bottom", sideOffset: 8, align: "start" }DropdownMenuContentSettings | undefined

Positioning settings — `side`, `align`, `sideOffset` control where the menu docks relative to its trigger.

defaultOpenfalseboolean | undefined

Initial open state when uncontrolled.

descriptionKey"description"string | undefined

Key on each item used as the rendered description.

disabledboolean | undefined

Disable the trigger.

itemsDropdownMenuItem[] | DropdownMenuItem[][] | undefined

Flat list, OR a nested array where each inner array becomes a group separated by a `<DropdownMenuSeparator>`.

labelKey"label"string | undefined

Key on each item used as the rendered label.

loadingIconstring | undefined

Iconify name for the loading spinner. Defaults to `appConfig.ui.icons.loading`.

modaltrueboolean | undefined

Modality. When `true`, taps outside the menu are blocked from reaching the underlying app.

openboolean | undefined

Controlled open state.

size"md" | "sm" | "lg" | "xl" | undefined
uiPartial<Record<"item" | "content" | "label" | "group" | "separator" | "itemLeadingIcon" | "itemLeadingAvatar" | "itemTrailing" | "itemLabel" | "itemLeadingAvatarSize" | "itemWrapper" | "itemDescription", ClassNameValue>> | undefined

Emits

EventPayload
update:open[value: boolean]

Slots

SlotBindings
default{ open: boolean; }

Trigger content. `DropdownMenuTrigger` toggles open state on tap; do NOT also bind a `@tap` handler that sets the open state.

itemDropdownMenuItemSlotProps

Custom rendering for every item (replaces all per-section defaults).

item-leadingDropdownMenuItemSlotProps

Custom rendering for every item's leading slot.

item-labelDropdownMenuItemSlotProps

Custom rendering for every item's label.

item-descriptionDropdownMenuItemSlotProps

Custom rendering for every item's description.

item-trailingDropdownMenuItemSlotProps

Custom rendering for every item's trailing slot.

Styling and theming

Override globally through appConfig.ui.dropdownMenu or per instance with ui.

UI slotPurpose
contentMenu panel surface, border, elevation, and scrolling.
groupPadding around a group of rows.
labelNon-interactive group heading.
separatorDivider between rows or groups.
itemRow layout, radius, and disabled state.
itemWrapperColumn holding the label and description.
itemLabel / itemDescriptionRow text.
itemLeadingIcon / itemLeadingAvatarLeading icon or avatar.
itemTrailingTrailing 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 useElementRect on 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.
  • Select for choosing a value rather than running an action.
  • Popover for arbitrary anchored content.
  • Drawer for an action list presented from an edge.