Menu triggered by a button, with items, groups, checkboxes, radio items and submenus.
Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/dropdown-menu.jsonInstalls to @/components/ui/dropdown-menu.
Manual installation
Install the dependencies:
pnpm add @base-ui/react@^1.8.0 lucide-react@^1.48.0Copy the source into your project:
'use client'
import { Menu as MenuPrimitive } from '@base-ui/react/menu'
import { CheckIcon, ChevronRightIcon } from 'lucide-react'
import * as React from 'react'
import { cn } from '@/lib/utils'
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
}
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
}
function DropdownMenuContent({
align = 'start',
alignOffset = 0,
side = 'bottom',
sideOffset = 4,
className,
...props
}: MenuPrimitive.Popup.Props & Pick<MenuPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'>) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
'z-50 max-h-(--available-height) w-max max-w-(--available-width) min-w-[max(8rem,var(--anchor-width))] origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-border duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95',
className,
)}
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
)
}
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
}
function DropdownMenuLabel({
className,
inset,
...props
}: MenuPrimitive.GroupLabel.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn('px-1.5 py-1 text-xs font-medium data-inset:pl-7', className)}
{...props}
/>
)
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean
variant?: 'default' | 'destructive'
}) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:*:[svg]:!text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:!text-destructive",
className,
)}
{...props}
/>
)
}
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" aria-hidden="true" />
</MenuPrimitive.SubmenuTrigger>
)
}
function DropdownMenuSubContent({
align = 'start',
alignOffset = -3,
side = 'right',
sideOffset = 0,
className,
...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
className={cn(
'w-auto min-w-[96px] rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-border duration-100 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
className,
)}
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: MenuPrimitive.CheckboxItem.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon aria-hidden="true" />
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />
}
function DropdownMenuRadioItem({
className,
children,
inset,
...props
}: MenuPrimitive.RadioItem.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<CheckIcon aria-hidden="true" />
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
)
}
function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn('-mx-1 my-1 h-px bg-border', className)}
{...props}
/>
)
}
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}A dropdown menu component that displays a menu when triggered by a button. Built on top of Base UI Menu, it follows WAI-ARIA design patterns and provides a fully accessible dropdown menu experience.
Features
- Keyboard navigation
- Type-ahead support
- Screen reader support
- Submenu support
- Checkbox items
- Radio group items
- Keyboard shortcuts (
DropdownMenuShortcut) - Destructive item variant (
variant="destructive") - Inset items, labels, checkbox and radio items (
inset) - Icons are sized automatically (
size-4), no extra classes needed - Content is as wide as its longest item, never narrower than the trigger or 8rem and never wider than the space available, and aligns to
start
Usage
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuCheckboxItem,
} from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'
import { SettingsIcon, Trash2Icon, UserIcon } from 'lucide-react'
// Basic usage
export function BasicDropdownMenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="ghost" />}>Ingrid Hansen</DropdownMenuTrigger>
<DropdownMenuContent className="w-60">
<DropdownMenuGroup>
<DropdownMenuLabel>My account</DropdownMenuLabel>
<DropdownMenuItem>
<UserIcon />
My profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<SettingsIcon />
Settings
<DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<Trash2Icon />
Remove delegation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
// With submenu
export function DropdownMenuWithSubmenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button />}>New</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Case</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Document from template</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Decision to grant</DropdownMenuItem>
<DropdownMenuItem>Neighbour notice</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
)
}
// With checkboxes
export function DropdownMenuWithCheckboxes() {
const [checked, setChecked] = React.useState(true)
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>Columns</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuCheckboxItem
checked={checked}
onCheckedChange={setChecked}
>
Case officer
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
)
}Examples
Basic Dropdown Menu
An account menu for a case officer, with icons and keyboard shortcuts.
With Submenu
A "New" menu with nested submenus for document templates and sharing.
With Checkboxes
Checkbox items that choose which columns the case list shows. Checkbox and radio items keep the menu open by default; pass closeOnClick to close it on selection.
With Radio Items
Radio items for the sort order. The trigger shows the current choice.
Destructive and Disabled Items
Use variant="destructive" for dangerous actions and disabled to make an item non-interactive. Here the menu sits in an Item for a document.
Content Width
Without a width class the menu is sized by its content and is at least as wide as the trigger. A menu on a wide trigger
lines up with it, and a menu opened from an icon button grows to fit its labels instead of wrapping them. Set a width
with className, such as w-56, when you want a fixed size.
Inset
Use inset to align items without an icon with items that have one.
Disabled
A dropdown menu showing disabled states of item, radio item, checkbox item and item in submenu.
Props
DropdownMenu
defaultOpen- The open state when initially renderedopen- The controlled open stateonOpenChange- Event handler for open state changesmodal- Whether the menu is modal (blocks interactions outside)
DropdownMenuTrigger
render- Render the trigger as another element, e.g.render={<Button variant="outline" />}children- The content of the trigger
DropdownMenuContent
children- Content to display in the menuclassName- Additional CSS classes (the content is sized by its items and is at least as wide as the trigger; set a fixed width with e.g.w-56)sideOffset- Distance from trigger (default4)align- Alignment relative to trigger (default'start')side- Side of the trigger to open on (default'bottom')
DropdownMenuItem
variant-'default' | 'destructive'(default'default')inset- Whether to indent the itemdisabled- Whether the item is disabledonClick- Event handler for selectioncloseOnClick- Whether clicking the item closes the menu (defaulttrue)children- Item content
DropdownMenuCheckboxItem
checked- The checked state (a check icon is shown on the right)onCheckedChange- Event handler for checked state changescloseOnClick- Whether clicking the item closes the menu (defaultfalse)inset- Whether to indent the itemdisabled- Whether the item is disabledchildren- Item content
DropdownMenuRadioItem
value- The value of the radio item (the selected item shows a check icon on the right)inset- Whether to indent the itemdisabled- Whether the item is disabledchildren- Item content
DropdownMenuLabel
inset- Whether to indent the labelchildren- Label content
A label names its group, so it must be placed inside a DropdownMenuGroup or DropdownMenuRadioGroup.
DropdownMenuSubTrigger
inset- Whether to indent the sub triggerchildren- Trigger content (a chevron is appended automatically)
DropdownMenuSeparator
className- Additional CSS classes
DropdownMenuShortcut
children- Shortcut textclassName- Additional CSS classes
Accessibility
The DropdownMenu component follows WAI-ARIA guidelines:
- Uses proper ARIA roles (
menu,menuitem,menuitemcheckbox,menuitemradio) - Supports keyboard navigation:
Space/Enter: Select item↑/↓: Navigate items→: Open submenu←: Close submenuEsc: Close menu
- Announces menu state changes
- Maintains focus management
- Provides visual feedback for interaction states
Best Practices
-
Menu Structure:
- Group related items
- Use clear labels
- Keep items concise
- Provide helpful shortcuts
-
Visual Design:
- Maintain consistent spacing
- Use clear visual hierarchy
- Provide feedback for actions
- Consider mobile interactions
-
Interaction:
- Manage focus properly
- Consider animations
- Test edge cases
-
Accessibility:
- Write descriptive labels
- Use semantic markup
- Test with screen readers
- Verify keyboard navigation
Related Components
- Context Menu
- Menubar
- Select
- Command