Dropdown Menu

Displays a menu to the user — such as a set of actions or functions — triggered by a button.

Examples

Basic

PropDefaultTypeDescription
items[]DropdownMenuItemProps[]The items to display in the dropdown-menu.
label-stringThe label to display in the dropdown-menu.
defaultOpenfalsebooleanThe open state of the dropdown menu when it is initially rendered. Use when you do not need to control its open state.
dirltrltr, rtlThe reading direction of the combobox when applicable. If omitted, inherits globally from ConfigProvider or assumes LTR (left-to-right) reading mode.
modaltruebooleanThe modality of the dropdown menu. When set to true, interaction with outside elements will be disabled and only menu content will be visible to screen readers.
openfalsebooleanThe controlled open state of the menu. Can be used as v-model:open.
Preview
Code

Inset

PropDefaultTypeDescription
insetfalsebooleanSet the dropdown-menu to be inset.
Preview
Code

Variant and Color

PropDefaultTypeDescription
dropdown-menusolid-white{variant}-{color}Change the color of the dropdown-menu.
dropdown-menu-itemgray{color}Change the color of the dropdown-menu item.
_dropdown-menu-trigger.dropdown-menusolid-white{variant}-{color}Change the color of the dropdown-menu trigger.
_dropdown-menu-item.dropdown-menu-itemgray{color}Change the color of the dropdown-menu item.
Preview
Code

Size

Adjust the dropdown-menu size without limits. Use breakpoints (e.g., sm:sm, xs:lg) for responsive sizes or states (e.g., hover:lg, focus:3xl) for state-based sizes.

PropDefaultTypeDescription
sizesmstringAdjusts the overall size of the dropdown-menu component.
_dropdownMenuItem.sizesmstringCustomizes the size of each item within the dropdown-menu dropdown.
_dropdownMenuTrigger.sizesmstringModifies the size of the dropdown-menu trigger element.
_dropdownMenuLabel.sizesmstringAdjusts the size of the dropdown-menu label.
Preview
Code

Slots

NamePropsDescription
trigger-The trigger slot.
itemitemThe item slot.
sub-trigger-The sub-trigger slot.
contentitemsThe content slot.
labellabelThe label slot.
groupitemsThe group slot.
Preview
Code

Presets

shortcuts/dropdown-menu.ts
type DropdownMenuPrefix = 'dropdown-menu'

export const staticDropdownMenu: Record<`${DropdownMenuPrefix}-${string}` | DropdownMenuPrefix, string> = {
  // configurations
  'dropdown-menu': '',
  'dropdown-menu-default-variant': 'btn-solid-white',

  // dropdown-menu-trigger
  'dropdown-menu-trigger': '',
  'dropdown-menu-trigger-leading': '',
  'dropdown-menu-trigger-trailing': 'ml-auto',

  // dropdown-menu-content
  'dropdown-menu-content': 'z-50 min-w-32 overflow-hidden rounded-md border border-base bg-popover p-1 text-popover shadow-md',

  // dropdown-menu-item
  'dropdown-menu-item-base': 'text-left transition-color focus-visible:outline-0',
  'dropdown-menu-item-leading': 'text-1em',
  'dropdown-menu-item-trailing': 'ml-auto opacity-75 text-1em',

  // dropdown-menu-label
  'dropdown-menu-label': 'px-2 py-1.5 text-1em font-semibold',

  // dropdown-menu-separator
  'dropdown-menu-separator-root': 'relative -mx-1',
  'dropdown-menu-separator': '',

  // dropdown-menu-shortcut
  'dropdown-menu-shortcut': 'pl-10 ml-auto text-0.875em tracking-widest n-disabled space-x-0.5',

  // dropdown-menu-group
  'dropdown-menu-group': '',

  // dropdown-menu-sub
  'dropdown-menu-sub-trigger': 'transition-color focus-visible:outline-0',
  'dropdown-menu-sub-trigger-leading': 'text-1em',
  'dropdown-menu-sub-trigger-trailing': 'ml-auto opacity-75 text-1em',
  'dropdown-menu-sub-trigger-trailing-icon': 'i-lucide-chevron-right',
  'dropdown-menu-sub-content': 'z-50 min-w-32 overflow-hidden rounded-md border border-base bg-popover p-1 text-popover shadow-lg',

}

export const dynamicDropdownMenu = [
  [/^dropdown-menu-([^-]+)-([^-]+)$/, ([, v = 'solid', c = 'white']) => `btn-${v}-${c}`],

  [/^dropdown-menu-item(?:-(\S+))?$/, ([, c = 'gray']) => `focus:bg-${c}-100 focus:text-${c}-800 dark:focus:bg-${c}-800 dark:focus:text-${c}-100 data-[state=open]:bg-${c}-100 dark:data-[state=open]:bg-${c}-800`],
]

export const dropdowMenu = [
  ...dynamicDropdownMenu,
  staticDropdownMenu,
]

Props

types/dropdown-menu.ts
import type {
  DropdownMenuContentProps,
  DropdownMenuGroupProps,
  DropdownMenuLabelProps,
  DropdownMenuRootProps,
  DropdownMenuSeparatorProps,
  DropdownMenuSubContentProps,
  DropdownMenuSubTriggerProps,
  DropdownMenuTriggerProps,
} from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import type { NButtonProps } from './button'
import type { NSeparatorProps } from './separator'

/**
 * Base extensions for dropdown menu components.
 */
interface BaseExtensions {
  /** CSS class for the component */
  class?: HTMLAttributes['class']
  /** Size of the component */
  size?: HTMLAttributes['class']
}

/**
 * Props for the NDropdownMenu component.
 */
export interface NDropdownMenuProps extends
  Omit<NDropdownMenuRootProps, 'class' | 'size'>,
  Omit<NDropdownMenuTriggerProps, 'una'>,
  Pick<NDropdownMenuItemProps, 'shortcut' | 'dropdownMenuItem'> {
  /** Label for the menu */
  menuLabel?: string
  /** Items in the dropdown menu */
  items?: NDropdownMenuProps[]
  /** Whether the menu is inset */
  inset?: boolean

  // Subcomponents
  /** Props for the dropdown menu root */
  _dropdownMenuRoot?: Partial<NDropdownMenuRootProps>
  /** Props for the dropdown menu item */
  _dropdownMenuItem?: Partial<NDropdownMenuItemProps>
  /** Props for the dropdown menu trigger */
  _dropdownMenuTrigger?: Partial<NDropdownMenuTriggerProps>
  /** Props for the dropdown menu content */
  _dropdownMenuContent?: Partial<NDropdownMenuContentProps>
  /** Props for the dropdown menu sub-content */
  _dropdownMenuSubContent?: Partial<NDropdownMenuSubContentProps>
  /** Props for the dropdown menu label */
  _dropdownMenuLabel?: Partial<NDropdownMenuLabelProps>
  /** Props for the dropdown menu separator */
  _dropdownMenuSeparator?: Partial<NDropdownMenuSeparatorProps>
  /** Props for the dropdown menu group */
  _dropdownMenuGroup?: Partial<NDropdownMenuGroupProps>
  /** Props for the dropdown menu sub-trigger */
  _dropdownMenuSubTrigger?: Partial<NDropdownMenuSubTriggerProps>

  /** Additional properties for the una component */
  una?: NDropdownMenuUnaProps & NButtonProps['una']
}

/**
 * Props for the NDropdownMenuRoot component.
 */
export interface NDropdownMenuRootProps extends BaseExtensions, DropdownMenuRootProps {
  /** Additional properties for the una component */
  una?: NDropdownMenuUnaProps['dropdownMenuRoot']
}

/**
 * Props for the NDropdownMenuTrigger component.
 */
export interface NDropdownMenuTriggerProps extends NButtonProps, DropdownMenuTriggerProps {
  /** Additional properties for the una component */
  una?: NDropdownMenuUnaProps['dropdownMenuTrigger'] & NButtonProps['una']
}

/**
 * Props for the NDropdownMenuContent component.
 */
export interface NDropdownMenuContentProps extends BaseExtensions, DropdownMenuContentProps {
  /** Additional properties for the una component */
  una?: NDropdownMenuUnaProps['dropdownMenuContent']
}

/**
 * Props for the NDropdownMenuLabel component.
 */
export interface NDropdownMenuLabelProps extends BaseExtensions, DropdownMenuLabelProps {
  /** Whether the label is inset */
  inset?: boolean
  /** Size of the label */
  size?: HTMLAttributes['class']
  /** Additional properties for the una component */
  una?: NDropdownMenuUnaProps['dropdownMenuLabel']
}

/**
 * Props for the NDropdownMenuSeparator component.
 */
export interface NDropdownMenuSeparatorProps extends DropdownMenuSeparatorProps, NSeparatorProps {
  /** Additional properties for the una component */
  una?: NDropdownMenuUnaProps['dropdownMenuSeparator'] & NSeparatorProps['una']
}

/**
 * Props for the NDropdownMenuGroup component.
 */
export interface NDropdownMenuGroupProps extends BaseExtensions, DropdownMenuGroupProps {
  /** Additional properties for the una component */
  una?: NDropdownMenuUnaProps['dropdownMenuGroup']
}

/**
 * Props for the NDropdownMenuSubContent component.
 */
export interface NDropdownMenuSubContentProps extends BaseExtensions, DropdownMenuSubContentProps {
  /** Additional properties for the una component */
  una?: NDropdownMenuUnaProps['dropdownMenuSubContent']
}

/**
 * Props for the NDropdownMenuItem component.
 */
export interface NDropdownMenuItemProps extends NButtonProps {
  /** Dropdown menu item */
  dropdownMenuItem?: HTMLAttributes['class']
  /** Whether the item is inset */
  inset?: boolean
  /** Shortcut key for the item */
  shortcut?: string
  /** Additional properties for the una component */
  una?: NDropdownMenuUnaProps['dropdownMenuItem'] & NButtonProps['una']
}

/**
 * Props for the NDropdownMenuSubTrigger component.
 */
export interface NDropdownMenuSubTriggerProps extends NButtonProps, DropdownMenuSubTriggerProps {
  /** Dropdown menu item */
  dropdownMenuItem?: HTMLAttributes['class']
  /** Whether the sub-trigger is inset */
  inset?: boolean
}

/**
 * Props for the NDropdownMenuShortcut component.
 */
export interface NDropdownMenuShortcutProps extends BaseExtensions {
  /** Shortcut key for the item */
  value?: string
  /** Additional properties for the una component */
  una?: NDropdownMenuUnaProps['dropdownMenuShortcut']
}

/**
 * Props for the NDropdownMenuUna component.
 */
interface NDropdownMenuUnaProps {
  /** CSS class for the dropdown menu content */
  dropdownMenuContent?: HTMLAttributes['class']
  /** CSS class for the dropdown menu sub-content */
  dropdownMenuSubContent?: HTMLAttributes['class']
  /** CSS class for the dropdown menu sub-trigger */
  dropdownMenuSubTrigger?: HTMLAttributes['class']
  /** CSS class for the dropdown menu trigger */
  dropdownMenuTrigger?: HTMLAttributes['class']
  /** CSS class for the dropdown menu label */
  dropdownMenuLabel?: HTMLAttributes['class']
  /** CSS class for the dropdown menu separator */
  dropdownMenuSeparator?: HTMLAttributes['class']
  /** CSS class for the dropdown menu group */
  dropdownMenuGroup?: HTMLAttributes['class']
  /** CSS class for the dropdown menu item */
  dropdownMenuItem?: HTMLAttributes['class']
  /** CSS class for the dropdown menu root */
  dropdownMenuRoot?: HTMLAttributes['class']
  /** CSS class for the dropdown menu shortcut */
  dropdownMenuShortcut?: HTMLAttributes['class']
}

Components

DropdownMenu.vue
DropdownMenuTrigger.vue
DropdownMenuItem.vue
DropdownMenuGroup.vue
DropdownMenuLabel.vue
DropdownMenuSeparator.vue
DropdownMenuContent.vue
DropdownMenuSub.vue
DropdownMenuSubTrigger
DropdownMenuSubContent
<script setup lang="ts">
import type { DropdownMenuContentEmits, DropdownMenuRootEmits } from 'radix-vue'
import type { NDropdownMenuProps } from '../../../types'
import { createReusableTemplate, reactivePick } from '@vueuse/core'
import { DropdownMenuPortal, useForwardPropsEmits } from 'radix-vue'
import { omitProps } from '../../../utils'
import DropdownMenuContent from './DropdownMenuContent.vue'
import DropdownMenuGroup from './DropdownMenuGroup.vue'
import DropdownMenuItem from './DropdownMenuItem.vue'
import DropdownMenuLabel from './DropdownMenuLabel.vue'
import DropdownMenuRoot from './DropdownMenuRoot.vue'
import DropdownMenuSeparator from './DropdownMenuSeparator.vue'
import DropdownMenuSub from './DropdownMenuSub.vue'
import DropdownMenuSubContent from './DropdownMenuSubContent.vue'
import DropdownMenuSubTrigger from './DropdownMenuSubTrigger.vue'
import DropdownMenuTrigger from './DropdownMenuTrigger.vue'

const props = defineProps<NDropdownMenuProps>()
const emits = defineEmits<DropdownMenuRootEmits & DropdownMenuContentEmits>()
const forwarded = useForwardPropsEmits(props, emits)

const [DefineMenuSub, ReuseMenuSub] = createReusableTemplate<NDropdownMenuProps>()
</script>

<template>
  <DropdownMenuRoot
    v-bind="reactivePick(forwarded, ['defaultOpen', 'open', 'modal', 'dir'])"
  >
    <DropdownMenuTrigger
      v-bind="omitProps({ ...forwarded, ...forwarded._dropdownMenuTrigger }, [
        'dropdownMenuItem',
        'items',
        'menuLabel',
        '_dropdownMenuItem',
        '_dropdownMenuContent',
        '_dropdownMenuLabel',
        '_dropdownMenuSeparator',
        '_dropdownMenuGroup',
        '_dropdownMenuTrigger',
        '_dropdownMenuSubTrigger',
        '_dropdownMenuSubContent',
      ])"
    >
      <slot />
    </DropdownMenuTrigger>

    <DropdownMenuContent
      v-bind="forwarded._dropdownMenuContent"
    >
      <slot name="content">
        <template
          v-if="menuLabel || $slots['menu-label']"
        >
          <DropdownMenuLabel
            :size
            :inset
            :una="forwarded.una?.dropdownMenuLabel"
            v-bind="forwarded._dropdownMenuLabel"
          >
            <slot name="menu-label">
              {{ menuLabel }}
            </slot>
          </DropdownMenuLabel>
          <DropdownMenuSeparator
            :una="forwarded.una?.dropdownMenuSeparator"
            v-bind="forwarded._dropdownMenuSeparator"
          />
        </template>

        <slot name="items" :items>
          <DropdownMenuGroup
            :una="forwarded.una?.dropdownMenuGroup"
            v-bind="forwarded._dropdownMenuGroup"
          >
            <template
              v-for="item in items"
              :key="item.label"
            >
              <slot
                v-if="!item.items && item.label"
                :name="`item-${item.label}`"
              >
                <DropdownMenuItem
                  :size
                  :inset
                  :dropdown-menu-item
                  :una="forwarded.una?.dropdownMenuItem"
                  v-bind="{ ...item, ...forwarded._dropdownMenuItem, ...item._dropdownMenuItem }"
                />
              </slot>

              <DropdownMenuSeparator
                v-else-if="!item.label && !item.items"
                :una="forwarded.una?.dropdownMenuSeparator"
                v-bind="{ ...forwarded._dropdownMenuSeparator, ...item._dropdownMenuSeparator }"
              />

              <ReuseMenuSub
                v-else
                v-bind="item"
              />
            </template>
          </DropdownMenuGroup>
        </slot>
      </slot>
    </DropdownMenuContent>
  </DropdownMenuRoot>

  <DefineMenuSub
    v-slot="subProps"
    as="div"
  >
    <template
      v-if="subProps.menuLabel"
    >
      <DropdownMenuLabel
        :size
        :inset
        :una="forwarded.una?.dropdownMenuLabel"
        v-bind="{ ...forwarded._dropdownMenuLabel, ...subProps._dropdownMenuLabel }"
      >
        {{ subProps.menuLabel }}
      </DropdownMenuLabel>
      <DropdownMenuSeparator
        :una="forwarded.una?.dropdownMenuSeparator"
        v-bind="{ ...forwarded._dropdownMenuSeparator, ...subProps._dropdownMenuSeparator }"
      />
    </template>

    <DropdownMenuGroup
      :una="forwarded.una?.dropdownMenuGroup"
      v-bind="{ ...forwarded._dropdownMenuGroup, ...subProps._dropdownMenuGroup }"
    >
      <DropdownMenuSub>
        <DropdownMenuSubTrigger
          :size
          :inset
          :una="forwarded.una?.dropdownMenuSubTrigger"
          :dropdown-menu-item
          v-bind="omitProps({
            ...subProps,
            ...forwarded._dropdownMenuSubTrigger,
            ...subProps._dropdownMenuSubTrigger,
          }, ['$slots'])"
        >
          <slot name="sub-trigger" :label="subProps.label" />
        </DropdownMenuSubTrigger>

        <DropdownMenuPortal>
          <DropdownMenuSubContent
            v-bind="subProps._dropdownMenuSubContent"
            :una="forwarded.una?.dropdownMenuSubContent"
          >
            <template
              v-for="subItem in subProps.items"
              :key="subItem.label"
            >
              <DropdownMenuItem
                v-if="!subItem.items && subItem.label"
                :size
                :inset
                :dropdown-menu-item
                :una="forwarded.una?.dropdownMenuItem"
                v-bind="{ ...subItem, ...forwarded._dropdownMenuItem, ...subItem._dropdownMenuItem }"
              >
                {{ subItem.label }}
              </DropdownMenuItem>

              <DropdownMenuSeparator
                v-else-if="!subItem.label && !subItem.items"
                :una="forwarded.una?.dropdownMenuSeparator"
                v-bind="{ ...forwarded._dropdownMenuSeparator, ...subItem._dropdownMenuSeparator }"
              />

              <ReuseMenuSub
                v-else
                v-bind="subItem"
              />
            </template>
          </DropdownMenuSubContent>
        </DropdownMenuPortal>
      </DropdownMenuSub>
    </DropdownMenuGroup>
  </DefineMenuSub>
</template>