Dropdown
A flexible menu component for actions. Handles groups, nested submenus, toggle rows, disabled items, custom triggers, and a built-in kebab pattern.
Simple
A plain actions menu with icons. The default trigger is an auto-generated <Button> — pass button: { label } to override its text.
<script setup lang="ts">
import { ref } from 'vue'
import { Dropdown, type DropdownOptions } from 'frappe-ui'
const lastAction = ref('')
const actions: DropdownOptions = [
{
label: 'Edit',
icon: 'lucide-pen',
onClick: () => (lastAction.value = 'Edited'),
},
{
label: 'Duplicate',
icon: 'lucide-copy',
onClick: () => (lastAction.value = 'Duplicated'),
},
{
label: 'Delete',
icon: 'lucide-trash-2',
theme: 'red',
onClick: () => (lastAction.value = 'Deleted'),
},
]
</script>
<template>
<div class="grid gap-3">
<Dropdown
:options="actions"
:button="{ icon: 'lucide-more-horizontal', label: 'Options' }"
/>
<div class="text-sm text-ink-gray-5">
{{ lastAction || 'Pick an action' }}
</div>
</div>
</template>With Shortcuts
Keyboard shortcuts rendered in the row suffix. Use the #item-suffix slot and a custom shortcut field on each item to keep the label clean and the hint secondary.
<script setup lang="ts">
import { Dropdown } from 'frappe-ui'
type FileAction = {
label: string
icon: string
shortcut: string[]
theme?: 'red'
onClick?: () => void
}
const actions: FileAction[] = [
{ label: 'New document', icon: 'lucide-plus', shortcut: ['⌘', 'N'] },
{ label: 'Rename', icon: 'lucide-pen', shortcut: ['F2'] },
{ label: 'Duplicate', icon: 'lucide-copy', shortcut: ['⌘', 'D'] },
{
label: 'Delete',
icon: 'lucide-trash-2',
shortcut: ['⌫'],
theme: 'red',
},
]
</script>
<template>
<Dropdown :options="actions" :button="{ label: 'File' }">
<template #item-suffix="{ item }">
<div
v-if="(item as FileAction).shortcut"
class="flex items-center gap-px"
>
<kbd
v-for="(key, i) in (item as FileAction).shortcut"
:key="i"
class="font-sans text-xs leading-4 text-ink-gray-6"
>
{{ key }}
</kbd>
</div>
</template>
</Dropdown>
</template>Submenus
Grouped actions with nested submenus — the "Share" path recurses into "Invite people" which recurses into channel targets. Groups are just { group, options } entries in the options array.
Switches
Toggle items live inside the menu using switch: true + switchValue. Clicking a switch row fires onClick(boolean) with the new value and keeps the menu open, so consumers can flip multiple settings in one sitting.
Kebab Menu
The classic row-actions pattern — a ghost icon button that opens a grouped menu. #trigger swaps in the LucideMoreHorizontal button, and the open slot prop keeps the button in its active state while the menu is open.
User Menu
A real workspace + profile menu — nested Apps / Theme submenus, account-group footer, and a completely custom trigger showing the workspace icon, product name, and current user.
Notes
- Prefer
#item-prefix,#item-label, and#item-suffixwhen you want to customize the standard dropdown row. - Use
#itemoritem.componentonly when you need to replace the entire row. Those escape hatches own the outer menu item element, so they should be reserved for exceptional cases.
API Reference
Show types
import type { Component, VNodeChild } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import type { ButtonProps } from '../Button'
export type DropdownTheme = 'gray' | 'red'
export type DropdownPlacement = 'left' | 'right' | 'center'
export type DropdownSide = 'top' | 'right' | 'bottom' | 'left'
export type DropdownAlign = 'start' | 'center' | 'end'
export type DropdownSlotFn<TProps> = (props: TProps) => VNodeChild
export interface DropdownItemSlots<TProps> {
/** Replaces the prefix region of the standard row shell. */
prefix?: DropdownSlotFn<TProps>
/** Replaces the label region of the standard row shell. */
label?: DropdownSlotFn<TProps>
/** Replaces the suffix region of the standard row shell. */
suffix?: DropdownSlotFn<TProps>
/** Replaces the entire row; mutually exclusive with `prefix` / `label` / `suffix`. */
item?: DropdownSlotFn<TProps>
}
export interface DropdownBaseOption {
/** Leading icon shown for the item row. */
icon?: string | Component | null
/** Secondary text shown below the label. */
description?: string
/** Marks the item as currently selected. */
selected?: boolean
/** Disables interaction for the item. */
disabled?: boolean
/** Visual theme applied to the item row. */
theme?: DropdownTheme
/** Named slot suffix used to resolve `item-*` slots dynamically. */
slot?: string
/** Per-item inline slot implementations for the row shell. */
slots?: DropdownItemSlots<DropdownItemSlotProps>
/** Condition used to omit an item from the final menu. */
condition?: () => boolean
[key: string]: any
}
export interface DropdownActionOption extends DropdownBaseOption {
/** Primary label shown for the action item. */
label: string
/** Router destination to navigate to when the item is clicked. */
route?: RouteLocationRaw
/** Click handler invoked when the action item is selected. */
onClick?: (event: PointerEvent) => void
submenu?: never
switch?: never
switchValue?: never
component?: never
}
export interface DropdownSwitchOption extends DropdownBaseOption {
/** Primary label shown for the switch item. */
label: string
/** Renders the item with a switch control. */
switch: true
/** Current boolean value for the switch item. */
switchValue?: boolean
/** Change handler invoked with the next switch value. */
onClick?: (value: boolean) => void
route?: never
submenu?: never
component?: never
}
export interface DropdownSubmenuOption extends DropdownBaseOption {
/** Primary label shown for the submenu trigger. */
label: string
/** Nested menu items rendered in the submenu. */
submenu: DropdownOptions
route?: never
onClick?: never
switch?: never
switchValue?: never
component?: never
}
export interface DropdownComponentOption extends DropdownBaseOption {
/**
* Custom component rendered in place of the standard menu row.
* @deprecated use `slots.item` for the full-row escape hatch instead
*/
component: any
/** Optional label used by custom renderers. */
label?: string
route?: never
submenu?: never
switch?: never
switchValue?: never
}
export interface DropdownGroupOption {
/** Stable key for the group wrapper. */
key?: string | number
/** Label rendered above the grouped items. */
group: string
/**
* Items rendered inside the group. Optional in the type so the
* deprecated `items` alias still typechecks; provide one of
* `options` or `items`.
*/
options?: DropdownOption[]
/**
* @deprecated use `options`. Accepted only as a back-compat alias for
* Dropdown's previous `{ group, items }` shape.
*/
items?: DropdownOption[]
/** Hides the group heading while preserving grouping. */
hideLabel?: boolean
/** Theme inherited by items in the group. */
theme?: DropdownTheme
}
export type DropdownOption =
| DropdownActionOption
| DropdownSwitchOption
| DropdownSubmenuOption
| DropdownComponentOption
export type DropdownItem = DropdownOption | DropdownGroupOption
export type DropdownOptions = Array<DropdownItem>
export interface DropdownProps {
/** Button configuration (label, icon, size, variant, etc.) */
button?: ButtonProps
/** Array of dropdown options or grouped options. */
options?: DropdownOptions
/** Controls the visibility of the dropdown. */
open?: boolean
/** Alignment of the dropdown content along the trigger edge. */
align?: DropdownAlign
/**
* Placement of the dropdown relative to the trigger.
* @deprecated use `align` instead; `placement` remains as a back-compat alias through v1.x
*/
placement?: DropdownPlacement
/** Side of the trigger the dropdown appears on. */
side?: DropdownSide
/** Offset in pixels between trigger and dropdown. */
offset?: number
/** Teleport target for dropdown portal content. */
portalTo?: string | HTMLElement
}
export interface DropdownSlotProps {
/** Closes the dropdown menu. */
close: () => void
}
export interface DropdownTriggerSlotProps extends DropdownSlotProps {
/** Whether the dropdown menu is currently open. */
open: boolean
/** Whether the trigger should render as disabled. */
disabled: boolean
[key: string]: any
}
export interface DropdownItemSlotProps extends DropdownSlotProps {
/** Item currently being rendered. */
item: DropdownOption
/** Whether the item is currently selected. */
selected: boolean
}
export interface DropdownGroupSlotProps {
/** Group currently being rendered. */
group: DropdownGroupOption
}
export interface DropdownSlots {
/** Alternate trigger renderer. */
default?: (props: DropdownTriggerSlotProps) => any
/** Explicit trigger slot renderer. */
trigger?: (props: DropdownTriggerSlotProps) => any
/** Replaces the entire item row. */
item?: (props: DropdownItemSlotProps) => any
/** Content rendered before the standard item label. */
'item-prefix'?: (props: DropdownItemSlotProps) => any
/** Content rendered for the standard item label area. */
'item-label'?: (props: DropdownItemSlotProps) => any
/** Content rendered after the standard item label. */
'item-suffix'?: (props: DropdownItemSlotProps) => any
/** Custom renderer for group labels. */
'group-label'?: (props: DropdownGroupSlotProps) => any
/** Fallback content rendered when no items are available. */
empty?: () => any
[slotName: string]: ((props: any) => any) | undefined
}
export interface DropdownEmits {
/** Fired when the dropdown open state changes. */
'update:open': [value: boolean]
}
export interface DropdownExposed {
/** Closes the dropdown menu. */
close: () => void
}| Prop | Default | Type |
|---|---|---|
button | — | ButtonProps Button configuration (label, icon, size, variant, etc.) |
options | [] | DropdownOptions Array of dropdown options or grouped options. |
open | false | boolean Controls the visibility of the dropdown. |
align | — | DropdownAlign Alignment of the dropdown content along the trigger edge. |
placement | — | Deprecated — use `align` instead; `placement` remains as a back-compat alias through v1.x |
side | "bottom" | DropdownSide Side of the trigger the dropdown appears on. |
offset | 4 | number Offset in pixels between trigger and dropdown. |
portalTo | "body" | string | HTMLElement Teleport target for dropdown portal content. |
| Slot | Payload |
|---|---|
default | DropdownTriggerSlotProps Alternate trigger renderer. |
trigger | DropdownTriggerSlotProps Explicit trigger slot renderer. |
item | DropdownItemSlotProps Replaces the entire item row. |
item-prefix | DropdownItemSlotProps Content rendered before the standard item label. |
item-label | DropdownItemSlotProps Content rendered for the standard item label area. |
item-suffix | DropdownItemSlotProps Content rendered after the standard item label. |
group-label | DropdownGroupSlotProps Custom renderer for group labels. |
empty | — Fallback content rendered when no items are available. |
| Event | Payload |
|---|---|
update:open | [value: boolean] Fired when the open state changes. |