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.

Pick an action
vue
<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.

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

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.

Ship Combobox v1
Due Friday · Engineering
Write migration guide
In review · Design
Audit selection components
Backlog · Platform

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.

Selected theme: system

Notes

  • Prefer #item-prefix, #item-label, and #item-suffix when you want to customize the standard dropdown row.
  • Use #item or item.component only 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
typescript
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.