Menu
Menus let people navigate or choose options from a menu
Usage
Use a menu to display a list of choices on a temporary surface, such as a set of overflow actions in a top app bar.
Menus allow users to make a selection from multiple options. They’re less prominent and take up less space than a set of radio buttons or choice chips.
Instalation
Run the following command:
npm i @radix-ui/react-dropdown-menu
Copy and paste the following code into your project.
'use client'
import * as React from 'react'
import * as MenuPrimitive from '@radix-ui/react-dropdown-menu'
import { cn } from '@/lib/utils'
import { Icon } from '@/components/ui/icon'
const MenuRoot = MenuPrimitive.Root
const MenuTrigger = MenuPrimitive.Trigger
const MenuGroup = MenuPrimitive.Group
const MenuPortal = MenuPrimitive.Portal
const MenuSub = MenuPrimitive.Sub
const MenuRadioGroup = MenuPrimitive.RadioGroup
interface MenuContentProps
extends React.ComponentPropsWithoutRef<typeof MenuPrimitive.Content> {
compact?: boolean
}
const MenuContent = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Content>,
MenuContentProps
>(({ className, sideOffset = 4, compact, ...props }, ref) => (
<MenuPrimitive.Portal>
<MenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
data-compact={compact ? '' : undefined}
className={cn(
'group/content z-50 min-w-[8rem] overflow-hidden rounded-xs bg-surfaceContainer py-2 text-onSurface shadow-md data-[state=closed]:duration-100 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',
compact && 'compact',
className
)}
{...props}
/>
</MenuPrimitive.Portal>
))
MenuContent.displayName = MenuPrimitive.Content.displayName
const MenuItem = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenuPrimitive.Item
ref={ref}
className={cn(
'relative flex h-12 cursor-pointer select-none items-center gap-3 px-3 py-2 text-label-lg text-onSurface outline-none transition-colors focus:bg-onSurface/8 active:bg-onSurface/12 data-[disabled]:pointer-events-none data-[disabled]:text-onSurface/38 group-data-[compact]/content:h-10 [&>i]:text-onSurfaceVariant [&>svg]:text-onSurfaceVariant [&>svg]:data-[disabled]:text-onSurface/38',
inset && 'pl-8',
className
)}
{...props}
/>
))
MenuItem.displayName = MenuPrimitive.Item.displayName
const MenuSubTrigger = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenuPrimitive.SubTrigger
ref={ref}
className={cn(
'relative flex h-12 cursor-pointer select-none items-center gap-3 px-3 py-2 text-label-lg text-onSurface outline-none transition-colors focus:bg-onSurface/8 active:bg-onSurface/12 data-[disabled]:pointer-events-none data-[disabled]:text-onSurface/38 group-data-[compact]/content:h-10 [&>svg]:text-onSurfaceVariant [&>svg]:data-[disabled]:text-onSurface/38',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<Icon symbol="arrow_right" className="ml-auto" />
</MenuPrimitive.SubTrigger>
))
MenuSubTrigger.displayName = MenuPrimitive.SubTrigger.displayName
interface MenuSubContentProps
extends React.ComponentPropsWithoutRef<typeof MenuPrimitive.SubContent> {
compact?: boolean
}
const MenuSubContent = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.SubContent>,
MenuSubContentProps
>(({ className, compact, ...props }, ref) => (
<MenuPrimitive.SubContent
ref={ref}
data-compact={compact ? '' : undefined}
className={cn(
'group/content z-50 min-w-[8rem] overflow-hidden rounded-xs bg-surfaceContainer py-2 text-onSurface shadow-md data-[state=closed]:duration-100 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-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
className
)}
{...props}
/>
))
MenuSubContent.displayName = MenuPrimitive.SubContent.displayName
const MenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.CheckboxItem>
>(({ className, children, ...props }, ref) => (
<MenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'group relative ml-auto flex h-12 cursor-pointer select-none items-center px-3 py-2 text-label-lg text-onSurface outline-none transition-colors hover:bg-onSurface/8 focus:bg-onSurface/8 active:bg-onSurface/12 data-[disabled]:pointer-events-none data-[disabled]:text-onSurface/38 group-data-[compact]/content:h-10',
className
)}
{...props}
>
<div className={cn('mr-4 grid size-5 shrink-0 place-items-center')}>
<MenuPrimitive.ItemIndicator
className={cn(
'flex h-[18px] w-[18px] items-center justify-center text-current transition-transform duration-200 animate-in data-[state=unchecked]:animate-out data-[state=checked]:zoom-in-0 data-[state=unchecked]:zoom-out-0 [&>i]:text-[18px] [&>svg]:h-[18px] [&>svg]:w-[18px]'
)}
>
<Icon symbol="check" />
</MenuPrimitive.ItemIndicator>
</div>
{children}
</MenuPrimitive.CheckboxItem>
))
MenuCheckboxItem.displayName = MenuPrimitive.CheckboxItem.displayName
const MenuRadioItem = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenuPrimitive.RadioItem
ref={ref}
className={cn(
'group relative ml-auto flex h-12 cursor-pointer select-none items-center px-3 py-2 text-label-lg text-onSurface outline-none transition-colors hover:bg-onSurface/8 focus:bg-onSurface/8 active:bg-onSurface/12 data-[disabled]:pointer-events-none data-[disabled]:text-onSurface/38 group-data-[compact]/content:h-10',
className
)}
{...props}
>
<div className={cn('mr-4 grid size-5 shrink-0 place-items-center')}>
<MenuPrimitive.ItemIndicator
className={cn(
'flex h-[18px] w-[18px] items-center justify-center text-current transition-transform duration-200 animate-in data-[state=unchecked]:animate-out data-[state=checked]:zoom-in-0 data-[state=unchecked]:zoom-out-0 [&>i]:text-[18px] [&>svg]:h-[18px] [&>svg]:w-[18px]'
)}
>
<Icon symbol="check" />
</MenuPrimitive.ItemIndicator>
</div>
{children}
</MenuPrimitive.RadioItem>
))
MenuRadioItem.displayName = MenuPrimitive.RadioItem.displayName
const MenuLabel = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenuPrimitive.Label
ref={ref}
className={cn('px-2 py-1.5 text-label-md', inset && 'pl-8', className)}
{...props}
/>
))
MenuLabel.displayName = MenuPrimitive.Label.displayName
const MenuDivider = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-outlineVariant', className)}
{...props}
/>
))
MenuDivider.displayName = MenuPrimitive.Separator.displayName
const MenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
'ml-auto text-label-lg tracking-widest text-onSurfaceVariant/50',
className
)}
{...props}
/>
)
}
MenuShortcut.displayName = 'MenuShortcut'
const Menu = Object.assign(MenuRoot, {
Portal: MenuPortal,
Trigger: MenuTrigger,
Group: MenuGroup,
SubMenu: MenuSub,
RadioGroup: MenuRadioGroup,
SubTrigger: MenuSubTrigger,
SubContent: MenuSubContent,
Content: MenuContent,
MenuItem: MenuItem,
CheckBoxItem: MenuCheckboxItem,
RadioItem: MenuRadioItem,
Label: MenuLabel,
Divider: MenuDivider,
Shortcut: MenuShortcut,
})
export {
Menu,
MenuPortal,
MenuTrigger,
MenuGroup,
MenuSub,
MenuRadioGroup,
MenuCheckboxItem,
MenuRadioItem,
MenuLabel,
MenuDivider,
MenuShortcut,
MenuContent,
MenuItem,
MenuSubTrigger,
MenuSubContent,
}
Update the import paths to match your project setup.
Examples
Menu Example
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Icon } from '@/components/ui/icon'
import { Menu } from '@/components/ui/menu'
const fontWeightsValues = [100, 200, 300, 400, 500, 600, 700]
export const MenuExample = () => {
const [lineSpacing, setLineSpacing] = useState('single')
const [fontWeights, setFontWeights] = useState(fontWeightsValues)
return (
<Menu>
<Menu.Trigger asChild>
<Button variant="outlined">Open menu</Button>
</Menu.Trigger>
<Menu.Content compact className="w-64">
<Menu.Group>
<Menu.MenuItem>
<Icon symbol="format_bold" />
<span>Bold</span>
<Menu.Shortcut>⌘B</Menu.Shortcut>
</Menu.MenuItem>
<Menu.MenuItem>
<Icon symbol="format_italic" />
<span>Italic</span>
<Menu.Shortcut>⌘I</Menu.Shortcut>
</Menu.MenuItem>
<Menu.MenuItem>
<Icon symbol="format_underlined" />
<span>Underline</span>
<Menu.Shortcut>⌘U</Menu.Shortcut>
</Menu.MenuItem>
<Menu.MenuItem>
<Icon symbol="format_strikethrough" />
<span>Strikethrough</span>
<Menu.Shortcut>⌘+Shift+X</Menu.Shortcut>
</Menu.MenuItem>
</Menu.Group>
<Menu.Divider />
<Menu.Group>
<Menu.SubMenu>
<Menu.SubTrigger>
<span>Line spacing</span>
</Menu.SubTrigger>
<Menu.Portal>
<Menu.SubContent compact>
<Menu.RadioGroup
value={lineSpacing}
onValueChange={setLineSpacing}
>
<Menu.RadioItem
value="single"
onSelect={(e) => e.preventDefault()}
>
<span>Single</span>
</Menu.RadioItem>
<Menu.RadioItem
value="1.15"
onSelect={(e) => e.preventDefault()}
>
<span>1.15</span>
</Menu.RadioItem>
<Menu.RadioItem
value="double"
onSelect={(e) => e.preventDefault()}
>
<span>Double</span>
</Menu.RadioItem>
<Menu.RadioItem
value="custom"
onSelect={(e) => e.preventDefault()}
>
<span>Custom: 1.2</span>
</Menu.RadioItem>
</Menu.RadioGroup>
<Menu.Divider />
<Menu.Group>
<Menu.MenuItem>
<span>Add space before paragraph</span>
</Menu.MenuItem>
<Menu.MenuItem>
<span>Add space after paragraph</span>
</Menu.MenuItem>
</Menu.Group>
<Menu.Divider />
<Menu.MenuItem>
<span>Custom spacing...</span>
</Menu.MenuItem>
</Menu.SubContent>
</Menu.Portal>
</Menu.SubMenu>
<Menu.SubMenu>
<Menu.SubTrigger>
<span>Font weights</span>
</Menu.SubTrigger>
<Menu.Portal>
<Menu.SubContent compact>
<Menu.Group>
{fontWeightsValues.map((v) => (
<Menu.CheckBoxItem
key={v}
checked={fontWeights.includes(v)}
onCheckedChange={(checked) => {
if (!checked) {
setFontWeights((prev) => prev.filter((w) => w !== v))
} else {
setFontWeights((prev) => [...prev, v])
}
}}
onSelect={(e) => e.preventDefault()}
>
<span>{v}</span>
</Menu.CheckBoxItem>
))}
</Menu.Group>
</Menu.SubContent>
</Menu.Portal>
</Menu.SubMenu>
</Menu.Group>
<Menu.Divider />
<Menu.Group>
<Menu.MenuItem>
<span>Undo</span>
</Menu.MenuItem>
<Menu.MenuItem disabled>
<span>Redo</span>
</Menu.MenuItem>
</Menu.Group>
</Menu.Content>
</Menu>
)
}