Menu
A dropdown list of actions tied to a trigger, with keyboard navigation, nested submenus, and optional checkbox or radio items.
Default
import {
CopyIcon,
PencilIcon,
ShareIcon,
StarIcon,
Trash2Icon,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Menu } from "@/components/ui/menu"
export default function Example() {
return (
<Menu.Root>
<Menu.Trigger render={<Button variant="outline">Song</Button>} />
<Menu.Portal>
<Menu.Positioner align="start">
<Menu.Popup>
<Menu.Item>
<PencilIcon />
Rename
<Menu.Shortcut>⌘R</Menu.Shortcut>
</Menu.Item>
<Menu.Item>
<CopyIcon />
Duplicate
<Menu.Shortcut>⌘D</Menu.Shortcut>
</Menu.Item>
<Menu.Item>
<StarIcon />
Favorite
<Menu.Shortcut>⌘F</Menu.Shortcut>
</Menu.Item>
<Menu.Separator />
<Menu.Item>
<ShareIcon />
Share
</Menu.Item>
<Menu.Separator />
<Menu.Item variant="danger">
<Trash2Icon />
Delete
<Menu.Shortcut>⌘⌫</Menu.Shortcut>
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
)
}Anatomy
import { Menu } from "@/components/ui/menu"
<Menu.Root>
<Menu.Trigger />
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Arrow />
<Menu.Item>
<Menu.Shortcut />
</Menu.Item>
<Menu.LinkItem />
<Menu.Separator />
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger />
</Menu.SubmenuRoot>
<Menu.Group>
<Menu.GroupLabel />
</Menu.Group>
<Menu.RadioGroup>
<Menu.GroupLabel />
<Menu.RadioItem>
<Menu.RadioItemIndicator />
</Menu.RadioItem>
</Menu.RadioGroup>
<Menu.CheckboxItem>
<Menu.CheckboxItemIndicator />
</Menu.CheckboxItem>
<Menu.Viewport />
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>Open on hover
Set openOnHover on the trigger so the menu opens when the pointer rests on it. Tune delay and closeDelay when the default timing feels too eager or too sticky.
import { FolderPlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Menu } from "@/components/ui/menu"
export default function Example() {
return (
<Menu.Root>
<Menu.Trigger
openOnHover
render={<Button variant="outline">Add to playlist</Button>}
/>
<Menu.Portal>
<Menu.Positioner align="start">
<Menu.Popup>
<Menu.Item>Get Up!</Menu.Item>
<Menu.Item>Inside Out</Menu.Item>
<Menu.Item>Night Beats</Menu.Item>
<Menu.Separator />
<Menu.Item>
<FolderPlusIcon />
New playlist…
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
)
}Checkbox items
Use Menu.CheckboxItem for independent on/off settings. The menu stays open by default so people can toggle several options in one visit.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Menu } from "@/components/ui/menu"
export default function Example() {
const [showMinimap, setShowMinimap] = React.useState(true)
const [showSearch, setShowSearch] = React.useState(true)
const [showSidebar, setShowSidebar] = React.useState(false)
return (
<Menu.Root>
<Menu.Trigger render={<Button variant="outline">Workspace</Button>} />
<Menu.Portal>
<Menu.Positioner align="start">
<Menu.Popup>
<Menu.CheckboxItem
checked={showMinimap}
onCheckedChange={setShowMinimap}
>
<Menu.CheckboxItemIndicator />
Minimap
</Menu.CheckboxItem>
<Menu.CheckboxItem
checked={showSearch}
onCheckedChange={setShowSearch}
>
<Menu.CheckboxItemIndicator />
Search
</Menu.CheckboxItem>
<Menu.CheckboxItem
checked={showSidebar}
onCheckedChange={setShowSidebar}
>
<Menu.CheckboxItemIndicator />
Sidebar
</Menu.CheckboxItem>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
)
}Radio items
Wrap exclusive choices in Menu.RadioGroup. Only one Menu.RadioItem can be selected at a time.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Menu } from "@/components/ui/menu"
export default function Example() {
const [value, setValue] = React.useState("date")
return (
<Menu.Root>
<Menu.Trigger render={<Button variant="outline">Sort</Button>} />
<Menu.Portal>
<Menu.Positioner align="start">
<Menu.Popup>
<Menu.RadioGroup value={value} onValueChange={setValue}>
<Menu.RadioItem value="date">
<Menu.RadioItemIndicator />
Date
</Menu.RadioItem>
<Menu.RadioItem value="name">
<Menu.RadioItemIndicator />
Name
</Menu.RadioItem>
<Menu.RadioItem value="type">
<Menu.RadioItemIndicator />
Type
</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
)
}Group labels
Menu.GroupLabel names a cluster of related items. Place it inside a radio group or a Menu.Group so the label is associated with those items for assistive tech.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Menu } from "@/components/ui/menu"
export default function Example() {
const [value, setValue] = React.useState("date")
const [showMinimap, setShowMinimap] = React.useState(true)
const [showSearch, setShowSearch] = React.useState(true)
const [showSidebar, setShowSidebar] = React.useState(false)
return (
<Menu.Root>
<Menu.Trigger render={<Button variant="outline">View</Button>} />
<Menu.Portal>
<Menu.Positioner align="start">
<Menu.Popup>
<Menu.RadioGroup value={value} onValueChange={setValue}>
<Menu.GroupLabel>Sort</Menu.GroupLabel>
<Menu.RadioItem value="date">
<Menu.RadioItemIndicator />
Date
</Menu.RadioItem>
<Menu.RadioItem value="name">
<Menu.RadioItemIndicator />
Name
</Menu.RadioItem>
<Menu.RadioItem value="type">
<Menu.RadioItemIndicator />
Type
</Menu.RadioItem>
</Menu.RadioGroup>
<Menu.Separator />
<Menu.Group>
<Menu.GroupLabel>Workspace</Menu.GroupLabel>
<Menu.CheckboxItem
checked={showMinimap}
onCheckedChange={setShowMinimap}
>
<Menu.CheckboxItemIndicator />
Minimap
</Menu.CheckboxItem>
<Menu.CheckboxItem
checked={showSearch}
onCheckedChange={setShowSearch}
>
<Menu.CheckboxItemIndicator />
Search
</Menu.CheckboxItem>
<Menu.CheckboxItem
checked={showSidebar}
onCheckedChange={setShowSidebar}
>
<Menu.CheckboxItemIndicator />
Sidebar
</Menu.CheckboxItem>
</Menu.Group>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
)
}Submenu
Nest another menu with Menu.SubmenuRoot and reuse Menu.Portal, Menu.Positioner, and Menu.Popup. Pass offset functions to sideOffset and alignOffset so the nested popup sits flush against the parent on every side.
import {
FolderPlusIcon,
ShareIcon,
StarIcon,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Menu } from "@/components/ui/menu"
function getSubmenuOffset({
side,
}: {
side?: "top" | "bottom" | "left" | "right" | "inline-end" | "inline-start"
}) {
return side === "top" || side === "bottom" ? 4 : -4
}
export default function Example() {
return (
<Menu.Root>
<Menu.Trigger render={<Button variant="outline">Song</Button>} />
<Menu.Portal>
<Menu.Positioner align="start">
<Menu.Popup>
<Menu.Item>
<StarIcon />
Add to Library
</Menu.Item>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger>Add to Playlist</Menu.SubmenuTrigger>
<Menu.Portal>
<Menu.Positioner
sideOffset={getSubmenuOffset}
alignOffset={getSubmenuOffset}
>
<Menu.Popup>
<Menu.Item>Get Up!</Menu.Item>
<Menu.Item>Inside Out</Menu.Item>
<Menu.Item>Night Beats</Menu.Item>
<Menu.Separator />
<Menu.Item>
<FolderPlusIcon />
New playlist…
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.SubmenuRoot>
<Menu.Separator />
<Menu.Item>Play Next</Menu.Item>
<Menu.Item>Play Last</Menu.Item>
<Menu.Separator />
<Menu.Item>
<ShareIcon />
Share
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
)
}Link items
Menu.LinkItem renders an anchor for navigation. Use it when the action should change the route instead of running a local handler.
import { LinkIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Menu } from "@/components/ui/menu"
export default function Example() {
return (
<Menu.Root>
<Menu.Trigger render={<Button variant="outline">Navigate</Button>} />
<Menu.Portal>
<Menu.Positioner align="start">
<Menu.Popup>
<Menu.LinkItem href="/components/alert-dialog">
<LinkIcon />
Alert Dialog
</Menu.LinkItem>
<Menu.LinkItem href="/components/autocomplete">
<LinkIcon />
Autocomplete
</Menu.LinkItem>
<Menu.LinkItem href="/components/tabs">
<LinkIcon />
Tabs
</Menu.LinkItem>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
)
}Open a dialog
Destructive menu items can open an AlertDialog for confirmation. Keep the dialog outside the menu tree and open it from the item onClick.
"use client"
import * as React from "react"
import { EyeIcon, ShareIcon, Trash2Icon } from "lucide-react"
import { AlertDialog } from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Menu } from "@/components/ui/menu"
export default function Example() {
const [dialogOpen, setDialogOpen] = React.useState(false)
return (
<>
<Menu.Root>
<Menu.Trigger render={<Button variant="outline">Mission</Button>} />
<Menu.Portal>
<Menu.Positioner align="start">
<Menu.Popup>
<Menu.Item>
<EyeIcon />
View flight log
</Menu.Item>
<Menu.Item>
<ShareIcon />
Share telemetry
</Menu.Item>
<Menu.Separator />
<Menu.Item variant="danger" onClick={() => setDialogOpen(true)}>
<Trash2Icon />
Discard flight log
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
<AlertDialog.Root open={dialogOpen} onOpenChange={setDialogOpen}>
<AlertDialog.Portal>
<AlertDialog.Backdrop />
<AlertDialog.Popup>
<AlertDialog.Header>
<AlertDialog.Title>Discard flight log?</AlertDialog.Title>
<AlertDialog.Description>
This mission data can't be recovered once you leave orbit.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Close
render={<Button variant="secondary">Cancel</Button>}
/>
<AlertDialog.Close
render={<Button variant="danger">Discard</Button>}
/>
</AlertDialog.Footer>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</>
)
}Detached trigger with payload
A handle from Menu.createHandle() connects triggers that live outside Menu.Root. Pass a payload on each trigger and read it back with a function child so one menu can render different content per opener.
"use client"
import { Button } from "@/components/ui/button"
import { Menu } from "@/components/ui/menu"
type MissionPayload = {
title: string
items: string[]
}
// Created once outside the component. Connects triggers to the menu
// without either one needing to be a descendant of the other.
const handle = Menu.createHandle<MissionPayload>()
const missionMenus = [
{
label: "Library",
payload: {
title: "Library",
items: ["Add to hangar", "Add to favorites", "Create playlist"],
},
},
{
label: "Playback",
payload: {
title: "Playback",
items: ["Play next", "Play last", "Add to queue"],
},
},
{
label: "Share",
payload: {
title: "Share",
items: ["Copy link", "Share with crew", "Export telemetry"],
},
},
]
export default function Example() {
return (
<>
{missionMenus.map((menu) => (
<Menu.Trigger
key={menu.label}
handle={handle}
payload={menu.payload}
render={<Button variant="outline">{menu.label}</Button>}
/>
))}
<Menu.Root<MissionPayload> handle={handle}>
{({ payload }) => (
<Menu.Portal>
<Menu.Positioner align="start">
<Menu.Popup>
<Menu.Group>
<Menu.GroupLabel>{payload?.title}</Menu.GroupLabel>
<Menu.Viewport>
{(payload?.items ?? []).map((item) => (
<Menu.Item key={item}>{item}</Menu.Item>
))}
</Menu.Viewport>
</Menu.Group>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
)}
</Menu.Root>
</>
)
}Arrow
Add Menu.Arrow inside the popup when you want a pointing cue toward the trigger. Bump sideOffset so the arrow has room to sit between the trigger and the panel.
import {
FolderPlusIcon,
ShareIcon,
StarIcon,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Menu } from "@/components/ui/menu"
export default function Example() {
return (
<Menu.Root>
<Menu.Trigger render={<Button variant="outline">Song</Button>} />
<Menu.Portal>
<Menu.Positioner
align="start"
sideOffset={({ side }) => (side === "top" ? 12 : 8)}
>
<Menu.Popup>
<Menu.Arrow />
<Menu.Item>
<StarIcon />
Add to Library
</Menu.Item>
<Menu.Item>
<FolderPlusIcon />
Add to Playlist
</Menu.Item>
<Menu.Separator />
<Menu.Item>Play Next</Menu.Item>
<Menu.Item>Play Last</Menu.Item>
<Menu.Separator />
<Menu.Item>
<ShareIcon />
Share
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
)
}Usage Guidelines
- Menu vs. Alert Dialog Use Menu for lists of actions tied to a control. Reach for Alert Dialog when an action needs an interrupting confirmation before it runs.
- Styled triggers The trigger part is unstyled by default. Compose it with
render={<Button variant="outline" />}so it matches the rest of the control set. - Destructive actions Mark irreversible items with
variant="danger". When the stakes are high, follow through with an Alert Dialog instead of acting immediately.
Props
Menu.Root
| Prop | Type | Default |
|---|---|---|
| open | boolean | — |
| defaultOpen | boolean | false |
| onOpenChange | (open: boolean, eventDetails) => void | — |
| handle | MenuHandle<Payload> | — |
| modal | boolean | true |
| loopFocus | boolean | true |
| orientation | 'vertical' | 'horizontal' | 'vertical' |
| disabled | boolean | false |
Menu.Trigger
| Prop | Type | Default |
|---|---|---|
| openOnHover | boolean | — |
| delay | number | 100 |
| closeDelay | number | 0 |
| payload | Payload | — |
| handle | MenuHandle<Payload> | — |
| disabled | boolean | false |
Menu.Positioner
| Prop | Type | Default |
|---|---|---|
| side | 'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start' | 'bottom' |
| sideOffset | number | OffsetFunction | 6 |
| align | 'start' | 'center' | 'end' | 'center' |
| alignOffset | number | OffsetFunction | 0 |
| collisionPadding | Padding | 5 |
Menu.Item
| Prop | Type | Default |
|---|---|---|
| variant | 'default' | 'danger' | 'default' |
| disabled | boolean | false |
| closeOnClick | boolean | true |
| label | string | — |
Menu.CheckboxItem
| Prop | Type | Default |
|---|---|---|
| checked | boolean | — |
| defaultChecked | boolean | false |
| onCheckedChange | (checked: boolean, eventDetails) => void | — |
| disabled | boolean | false |
| closeOnClick | boolean | false |
Menu.RadioItem
| Prop | Type | Default |
|---|---|---|
| value | any | — |
| disabled | boolean | false |
| closeOnClick | boolean | false |
| label | string | — |
This covers the parts used above. See the Base UI Menu docs for actionsRef, event details, CSS variables and data attributes.