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>
  )
}

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>
  )
}

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

PropTypeDefault
openboolean—
defaultOpenbooleanfalse
onOpenChange(open: boolean, eventDetails) => void—
handleMenuHandle<Payload>—
modalbooleantrue
loopFocusbooleantrue
orientation'vertical' | 'horizontal''vertical'
disabledbooleanfalse

Menu.Trigger

PropTypeDefault
openOnHoverboolean—
delaynumber100
closeDelaynumber0
payloadPayload—
handleMenuHandle<Payload>—
disabledbooleanfalse

Menu.Positioner

PropTypeDefault
side'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start''bottom'
sideOffsetnumber | OffsetFunction6
align'start' | 'center' | 'end''center'
alignOffsetnumber | OffsetFunction0
collisionPaddingPadding5

Menu.Item

PropTypeDefault
variant'default' | 'danger''default'
disabledbooleanfalse
closeOnClickbooleantrue
labelstring—

Menu.CheckboxItem

PropTypeDefault
checkedboolean—
defaultCheckedbooleanfalse
onCheckedChange(checked: boolean, eventDetails) => void—
disabledbooleanfalse
closeOnClickbooleanfalse

Menu.RadioItem

PropTypeDefault
valueany—
disabledbooleanfalse
closeOnClickbooleanfalse
labelstring—

This covers the parts used above. See the Base UI Menu docs for actionsRef, event details, CSS variables and data attributes.