Siteplane UI

Menu

Offer a compact list of actions and choices.

Markdown
"use client";

import {
  ArchiveIcon,
  CopyIcon,
  DownloadIcon,
  FolderIcon,
  FolderInputIcon,
  PencilIcon,
  ShareIcon,
  Trash2Icon,
} from "lucide-react";
import { Button } from "@/siteplane/components/button";
import {
  Menu,
  MenuGroup,
  MenuGroupLabel,
  MenuItem,
  MenuPopup,
  MenuSeparator,
  MenuShortcut,
  MenuSub,
  MenuSubPopup,
  MenuSubTrigger,
  MenuTrigger,
} from "@/siteplane/components/menu";

export default function Example() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="outline" />}>Actions</MenuTrigger>
      <MenuPopup>
        <MenuGroup>
          <MenuGroupLabel>Project</MenuGroupLabel>
          <MenuItem>
            <PencilIcon aria-hidden="true" />
            Edit
            <MenuShortcut>⌘E</MenuShortcut>
          </MenuItem>
          <MenuItem>
            <CopyIcon aria-hidden="true" />
            Duplicate
            <MenuShortcut>⌘D</MenuShortcut>
          </MenuItem>
          <MenuItem>
            <ShareIcon aria-hidden="true" />
            Share
          </MenuItem>
          <MenuItem disabled>
            <DownloadIcon aria-hidden="true" />
            Export
          </MenuItem>
        </MenuGroup>
        <MenuSeparator />
        <MenuSub>
          <MenuSubTrigger>
            <FolderInputIcon aria-hidden="true" />
            Move to
          </MenuSubTrigger>
          <MenuSubPopup>
            <MenuItem>
              <FolderIcon aria-hidden="true" />
              Marketing
            </MenuItem>
            <MenuItem>
              <FolderIcon aria-hidden="true" />
              Engineering
            </MenuItem>
            <MenuItem>
              <ArchiveIcon aria-hidden="true" />
              Archive
            </MenuItem>
          </MenuSubPopup>
        </MenuSub>
        <MenuSeparator />
        <MenuItem variant="destructive">
          <Trash2Icon aria-hidden="true" />
          Delete
          <MenuShortcut>⌘⌫</MenuShortcut>
        </MenuItem>
      </MenuPopup>
    </Menu>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/menu

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/avatar @siteplane/button

Usage

import { PencilIcon } from "lucide-react";
import {
  Menu,
  MenuTrigger,
  MenuPopup,
  MenuItem,
} from "@/siteplane/components/menu";
import { Button } from "@/siteplane/components/button";

<Menu>
  <MenuTrigger render={<Button />}>Actions</MenuTrigger>
  <MenuPopup>
    <MenuItem>
      <PencilIcon />
      Edit
    </MenuItem>
  </MenuPopup>
</Menu>;

Composition

Menu
├── MenuTrigger
└── MenuPopup
    ├── MenuGroup → MenuGroupLabel + items
    ├── MenuItem / CheckboxItem / RadioGroup
    ├── MenuSeparator
    └── MenuSub → SubTrigger + MenuSubPopup

Account menu

Open account actions from an avatar button: a Button with profile="avatar" makes the portrait itself the trigger, with no box around it. A group label shows the signed-in user, MenuLinkItem renders a real link, and a submenu holds the theme choice.

"use client";

import {
  CreditCardIcon,
  LifeBuoyIcon,
  LogOutIcon,
  SettingsIcon,
  SunMoonIcon,
  UserIcon,
} from "lucide-react";
import { useState } from "react";
import { Avatar, AvatarFallback } from "@/siteplane/components/avatar";
import { Button } from "@/siteplane/components/button";
import {
  Menu,
  MenuGroup,
  MenuGroupLabel,
  MenuItem,
  MenuLinkItem,
  MenuPopup,
  MenuRadioGroup,
  MenuRadioItem,
  MenuSeparator,
  MenuShortcut,
  MenuSub,
  MenuSubPopup,
  MenuSubTrigger,
  MenuTrigger,
} from "@/siteplane/components/menu";

export default function Example() {
  const [theme, setTheme] = useState("system");
  return (
    <Menu>
      <MenuTrigger
        render={
          <Button
            aria-label="Open account menu"
            profile="avatar"
            variant="ghost"
          />
        }
      >
        <Avatar>
          <AvatarFallback>ME</AvatarFallback>
        </Avatar>
      </MenuTrigger>
      <MenuPopup align="end">
        <MenuGroup>
          <MenuGroupLabel className="flex items-center gap-2">
            <Avatar aria-hidden="true">
              <AvatarFallback>ME</AvatarFallback>
            </Avatar>
            <span className="grid min-w-0">
              <span className="truncate text-foreground text-sm">
                Mara Ellis
              </span>
              <span className="truncate">mara@example.com</span>
            </span>
          </MenuGroupLabel>
          <MenuItem>
            <UserIcon aria-hidden="true" />
            Profile
          </MenuItem>
          <MenuItem>
            <CreditCardIcon aria-hidden="true" />
            Billing
          </MenuItem>
          <MenuItem>
            <SettingsIcon aria-hidden="true" />
            Settings
            <MenuShortcut>⌘,</MenuShortcut>
          </MenuItem>
        </MenuGroup>
        <MenuSub>
          <MenuSubTrigger>
            <SunMoonIcon aria-hidden="true" />
            Appearance
          </MenuSubTrigger>
          <MenuSubPopup>
            <MenuGroup>
              <MenuGroupLabel>Theme</MenuGroupLabel>
              <MenuRadioGroup value={theme} onValueChange={setTheme}>
                <MenuRadioItem value="system">System</MenuRadioItem>
                <MenuRadioItem value="light">Light</MenuRadioItem>
                <MenuRadioItem value="dark">Dark</MenuRadioItem>
              </MenuRadioGroup>
            </MenuGroup>
          </MenuSubPopup>
        </MenuSub>
        <MenuSeparator />
        <MenuLinkItem href="#help">
          <LifeBuoyIcon aria-hidden="true" />
          Help center
        </MenuLinkItem>
        <MenuSeparator />
        <MenuItem>
          <LogOutIcon aria-hidden="true" />
          Sign out
        </MenuItem>
      </MenuPopup>
    </Menu>
  );
}

Checkbox and radio items

Use MenuCheckboxItem for independent settings and MenuRadioGroup for one choice; both work controlled or with defaultChecked and defaultValue. variant="switch" only changes the indicator. The check draws in when an item is checked and out when it is not, the way the Checkbox check does, and appears instantly under reduced motion.

A radio item shows its check in the leading slot, where action rows show their icon. For identity rows that lead with a logo or an avatar, such as the workspaces of a switcher, set indicatorPosition="end": the row starts with its first child and ends with the check. The Sidebar examples use it for their workspace switcher.

"use client";

import { useState } from "react";
import { Button } from "@/siteplane/components/button";
import {
  Menu,
  MenuCheckboxItem,
  MenuGroup,
  MenuGroupLabel,
  MenuPopup,
  MenuRadioGroup,
  MenuRadioItem,
  MenuSeparator,
  MenuTrigger,
} from "@/siteplane/components/menu";

export default function Example() {
  const [email, setEmail] = useState(true);
  const [digest, setDigest] = useState("weekly");
  return (
    <Menu>
      <MenuTrigger render={<Button variant="outline" />}>
        Notifications
      </MenuTrigger>
      <MenuPopup>
        <MenuGroup>
          <MenuGroupLabel>Channels</MenuGroupLabel>
          <MenuCheckboxItem checked={email} onCheckedChange={setEmail}>
            Email
          </MenuCheckboxItem>
          <MenuCheckboxItem defaultChecked>Push notifications</MenuCheckboxItem>
        </MenuGroup>
        <MenuSeparator />
        <MenuGroup>
          <MenuGroupLabel>Email digest</MenuGroupLabel>
          <MenuRadioGroup value={digest} onValueChange={setDigest}>
            <MenuRadioItem value="daily">Daily</MenuRadioItem>
            <MenuRadioItem value="weekly">Weekly</MenuRadioItem>
            <MenuRadioItem value="never">Never</MenuRadioItem>
          </MenuRadioGroup>
        </MenuGroup>
        <MenuSeparator />
        <MenuCheckboxItem variant="switch">
          Pause notifications
        </MenuCheckboxItem>
      </MenuPopup>
    </Menu>
  );
}

Glass

Set variant="glass" on MenuPopup when the menu opens over a photo, a video or a color surface, and on a MenuSubPopup so its submenu matches. Rows, icons and their states stay the same. Glass shows every component with a glass variant.

The gradient only makes the glass visible. It is not part of the component.

"use client";

import { CopyIcon, PencilIcon, ShareIcon, Trash2Icon } from "lucide-react";
import { Button } from "@/siteplane/components/button";
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuSeparator,
  MenuTrigger,
} from "@/siteplane/components/menu";

export default function Example() {
  return (
    <Menu>
      <MenuTrigger render={<Button variant="glass" />}>Options</MenuTrigger>
      <MenuPopup variant="glass">
        <MenuItem>
          <PencilIcon aria-hidden="true" />
          Edit
        </MenuItem>
        <MenuItem>
          <CopyIcon aria-hidden="true" />
          Duplicate
        </MenuItem>
        <MenuItem>
          <ShareIcon aria-hidden="true" />
          Share
        </MenuItem>
        <MenuSeparator />
        <MenuItem variant="destructive">
          <Trash2Icon aria-hidden="true" />
          Delete
        </MenuItem>
      </MenuPopup>
    </Menu>
  );
}

API reference

Menu, MenuPortal and the group, radio and submenu parts follow Base UI Menu. Popup includes its Portal, Positioner and a scrolling viewport; do not add another Portal around it. LinkItem renders a native link. Shortcut renders kbd and does not register a shortcut. SubPopup opens on the inline-end side. MenuPrimitive exports the Base UI namespace. Styled className props are strings.

MenuCreateHandle is the detached-trigger handle factory. Shared menu style helpers are implementation support and are not re-exported from the Menu index.

Component properties
PropTypeDefaultDescription
MenuPopup.sidePositioner sidebottomPreferred edge.
MenuPopup.alignstart | center | endcenterAlignment.
MenuPopup.sideOffsetPositioner offset4Gap.
MenuPopup.alignOffsetPositioner offsetunsetAlignment offset.
MenuPopup.widthauto | trigger | content | at-least-triggerautoWidth relationship to content/trigger. trigger matches the trigger; every other width is at most 20rem, or the trigger's width for at-least-trigger when it is wider. Every width stays within the space beside the trigger; a longer row label ends in an ellipsis and a group label wraps.
MenuPopup.anchorPositioner anchortriggerCustom anchor.
MenuPopup.portalPropsPortal.PropsunsetPortal options.
MenuPopup.variantdefault | glassdefaultglass shows a tinted, blurred view of what lies behind the popup.
MenuItem.variantdefault | destructivedefaultAction tone. A destructive item colors its label and its icon red.
Item / GroupLabel / SubTrigger.insetbooleanunset (off)Reserve the icon slot so the label lines up with icon rows.
MenuCheckboxItem.variantdefault | switchdefaultCheck or switch indicator; retains menuitemcheckbox semantics.
MenuRadioItem.indicatorPositionstart | endstartWhere the check shows: in the leading slot, or at the end of an identity row that leads with a logo or an avatar.
MenuCheckboxItem.sizesm | md | lg | xllgSwitch size. Menu rows stay compact at every control size, so the switch does not follow the provider.
MenuSubPopup.align / sideOffsetalignment / offsetstart / 0Submenu positioning.
MenuSubPopup.alignOffsetoffset-5 unless align=centerSubmenu alignment correction.
MenuSubPopup.variantdefault | glassdefaultSet glass to match a glass parent menu.
open / defaultOpenbooleanfalseVisibility.
onOpenChange(open, eventDetails) => voidunsetVisibility callback.
Item.onClickMouseEventHandlerunsetApplication action.
CheckboxItem.checked / onCheckedChangeboolean / callbackuncontrolled / unsetControlled checked state.
RadioGroup.value / onValueChangestring / callbackuncontrolled / unsetControlled choice.
MenuLinkItem.closeOnClickbooleantrueCloses the menu after activating a link.
MenuLinkItem.variant / insettone / booleandefault / unsetLink-row presentation.

Modal, disabled, focus, dismissal and close-on-click behavior follow Base UI. A disabled item cannot be activated; the submenu trigger supplies the chevron. SubPopup accepts width plus the listed offsets, not arbitrary portal or anchor options.

The Menu module also exports DropdownMenu-prefixed aliases: DropdownMenu → Menu, DropdownMenuPortal → MenuPortal, DropdownMenuTrigger → MenuTrigger, DropdownMenuContent → MenuPopup, DropdownMenuGroup → MenuGroup, DropdownMenuItem → MenuItem, DropdownMenuCheckboxItem → MenuCheckboxItem, DropdownMenuRadioGroup → MenuRadioGroup, DropdownMenuRadioItem → MenuRadioItem, DropdownMenuLabel → MenuGroupLabel, DropdownMenuSeparator → MenuSeparator, DropdownMenuShortcut → MenuShortcut, DropdownMenuSub → MenuSub, DropdownMenuSubTrigger → MenuSubTrigger, DropdownMenuSubContent → MenuSubPopup, and DropdownMenuCreateHandle → MenuCreateHandle. They behave exactly like the Menu parts; see Dropdown Menu.

Read the installed source.

Usage notes

Start every action item with a fitting lucide icon, such as PencilIcon for Edit or Trash2Icon for Delete; checkbox and radio items show their own indicator instead. Arrow keys move through the items, including disabled ones, which cannot be activated. Enter activates an item, and Escape closes the menu with focus restoration. Use real links for navigation. Selection here is menu state, not native form submission. To open a dialog from a menu item, see Dialog.