๐ข Dropdown Menu
- Can be controlled or uncontrolled.
- Supports submenus with configurable reading direction.
- Supports items, labels, groups of items.
- Supports checkable items (single or multiple) with optional indeterminate state.
- Supports modal and non-modal modes.
- Customize side, alignment, offsets, collision handling.
- Optionally render a pointing arrow.
- Focus is fully managed.
- Full keyboard navigation.
- Typeahead support.
- Dismissing and layering behavior is highly customizable.
Basic
NDropdownMenu
is a component that can be used to display a list of actions or options.
Prop | Type | Default | Description |
---|---|---|---|
items | DropdownMenuItemProps[] | [] | The items to display in the dropdown-menu. |
label | string | undefined | The label to display in the dropdown-menu. |
All the props available in the Radix Vue Dropdown Menu are also
available via its subcomponents' prop names, e.g.,
_dropdown-menu-item
, _dropdown-menu-trigger
, etc. refer to
DropdownMenu Props for more details.Inset
inset
prop is used to set the dropdown-menu to be inset.
Variant and Color
dropdown-menu="{variant}-{color}"
is used to set the variant of the dropdown-menu. The default variant is soft-black
.
dropdown-menu-item="{color}"
is used to set the variant of the dropdown-menu item. The default variant is soft-black
.
Prop | Description |
---|---|
dropdown-menu | Set the dropdown-menu variant and color. |
_dropdown-menu-trigger.dropdown-menu | Set the dropdown-menu variant and color via _dropdown-menu-trigger . |
dropdown-menu-item | Set the dropdown-menu item variant and color. |
_dropdown-menu-item.dropdown-menu-item | Set the dropdown-menu item variant and color via _dropdown-menu-item . |
NDropdownMenuTrigger
is wrapped around the NButton component. This means that all the props and slots of
NButton
are available to use or through _dropdown-menu-trigger
prop.Size
Prop | Description |
---|---|
size | Set the dropdown-menu general size. |
_dropdownMenuTrigger.size | Set the trigger size only. |
_dropdownMenuItem.size | Set the item size only. |
_dropdownMenuLabel.size | Set the menu label size only. |
๐ You can freely adjust the size of the dropdown-menu using any size imaginable. No limits exist, and you can use
breakpoints
such assm:sm, xs:lg
to change size based on screen size orstates
such ashover:lg, focus:3xl
to change size based on input state and more.
The
height
and width
of the dropdown-menu scale depends on the dropdown-menu-size
. If you want to change the height
and
width
simultaneously, you can always customize it using utility classes.Slots
You can use the following slots to customize the dropdown-menu.
Name | Description | Props |
---|---|---|
trigger | The trigger slot. | - |
item | The item slot. | item |
sub-trigger | The sub-trigger slot. | - |
content | The content slot. | items |
label | The label slot. | label |
group | The group slot. | items |
Props
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']
}
Presets
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 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
// dropdown-menu-item
'dropdown-menu-item-base': 'text-left transition-color focus-visible:outline-0',
'dropdown-menu-item-leading': 'opacity-75 text-1.1em',
'dropdown-menu-item-trailing': 'ml-auto opacity-75 text-1.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': 'opacity-75 text-1.1em',
'dropdown-menu-sub-trigger-trailing': 'ml-auto opacity-75 text-1.1em',
'dropdown-menu-sub-content': 'z-50 min-w-32 overflow-hidden rounded-md border border-base bg-popover p-1 text-popover shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
}
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,
]
Component
<script setup lang="ts">
import type { DropdownMenuContentEmits, DropdownMenuRootEmits } from 'radix-vue'
import type { NDropdownMenuProps } from '../../../types'
import { createReusableTemplate } from '@vueuse/core'
import { DropdownMenuPortal, useForwardPropsEmits } from 'radix-vue'
import { omitProps, pickProps } 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="pickProps(forwarded, ['defaultOpen', 'open', 'modal', 'dir'])"
>
<slot>
<slot name="trigger">
<DropdownMenuTrigger
v-bind="omitProps({ ...forwarded, ...forwarded._dropdownMenuTrigger }, [
'dropdownMenuItem',
'items',
'menuLabel',
'_dropdownMenuItem',
'_dropdownMenuContent',
'_dropdownMenuLabel',
'_dropdownMenuSeparator',
'_dropdownMenuGroup',
'_dropdownMenuSubTrigger',
'_dropdownMenuSubContent',
])"
/>
</slot>
<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>
</slot>
</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>