Menu

A list of actions that opens from a button, with shortcuts, checks and submenus.

import { Button } from '@/components/panelui/button';
import { Menu } from '@/components/panelui/menu';

export default function MenuDemo() {
  return (
    <Menu>
      <Menu.Trigger render={<Button variant="outline" />}>Open</Menu.Trigger>
      <Menu.Content className="w-56">
        <Menu.Group>
          <Menu.GroupLabel>Workspace</Menu.GroupLabel>
          <Menu.Item>
            Profile
            <Menu.Shortcut>⇧⌘P</Menu.Shortcut>
          </Menu.Item>
          <Menu.Item>
            Plan and billing
            <Menu.Shortcut>⌘B</Menu.Shortcut>
          </Menu.Item>
          <Menu.Item>
            Preferences
            <Menu.Shortcut>⌘,</Menu.Shortcut>
          </Menu.Item>
        </Menu.Group>
        <Menu.Separator />
        <Menu.Group>
          <Menu.Item>Members</Menu.Item>
          <Menu.Sub>
            <Menu.SubTrigger>Invite people</Menu.SubTrigger>
            <Menu.SubContent>
              <Menu.Item>By email</Menu.Item>
              <Menu.Item>With a link</Menu.Item>
              <Menu.Separator />
              <Menu.Item>From another workspace</Menu.Item>
            </Menu.SubContent>
          </Menu.Sub>
          <Menu.Item>
            New project
            <Menu.Shortcut>⌘N</Menu.Shortcut>
          </Menu.Item>
        </Menu.Group>
        <Menu.Separator />
        <Menu.Item>Help centre</Menu.Item>
        <Menu.Item disabled>Status page</Menu.Item>
        <Menu.Separator />
        <Menu.Item>
          Sign out
          <Menu.Shortcut>⇧⌘Q</Menu.Shortcut>
        </Menu.Item>
      </Menu.Content>
    </Menu>
  );
}

Use it for actions on one thing that do not all fit beside it: a document's rename, share and delete, a row's more button. For choosing a value that then shows in the button, use Select.

Every row has the same shape: an icon, the label, and at its end a shortcut, a badge, a check or a submenu's chevron.

It opens from Menu.Trigger, which is a plain button until you give it render. Pass a Button there to draw it as one.

Installation

npx shadcn@latest add https://panelui.dev/r/web/menu.json

The source is copied into components/panelui/, where you can edit it.

npm install panelui-web

Import from panelui-web/menu instead of @/components/panelui/menu. Tailwind needs one line to read the package's classes, which is on the installation page.

Install the packages it uses:

npm install @base-ui/react tailwind-variants

Copy the source into components/panelui/menu.tsx:

Usage

import { Menu } from "@/components/panelui/menu"
<Menu>
  <Menu.Trigger render={<Button variant="outline" />}>Document</Menu.Trigger>
  <Menu.Content>
    <Menu.Item onClick={rename}>
      <PencilIcon />
      Rename
      <Menu.Shortcut>⌘R</Menu.Shortcut>
    </Menu.Item>
    <Menu.Separator />
    <Menu.Item variant="destructive">Delete</Menu.Item>
  </Menu.Content>
</Menu>

Composition

<Menu>
  <Menu.Trigger />
  <Menu.Content>
    <Menu.Item>
      <Menu.Description />
      <Menu.Shortcut />
      <Menu.Badge />
    </Menu.Item>
    <Menu.Sub>
      <Menu.SubTrigger />
      <Menu.SubContent />
    </Menu.Sub>
    <Menu.Separator />
    <Menu.Group>
      <Menu.GroupLabel />
      <Menu.CheckboxItem />
      <Menu.RadioGroup>
        <Menu.RadioItem />
      </Menu.RadioGroup>
    </Menu.Group>
  </Menu.Content>
</Menu>
  • Menu.Trigger — The button that opens the menu. render draws it as another element, such as a Button.
  • Menu.Content — The list, in a layer over the page. side and align place it against the trigger, and it moves to the other side when there is no room. anchor places it against another element instead.
  • Menu.Item — One action, which closes the menu when pressed. variant="destructive" is for one that cannot be undone.
  • Menu.CheckboxItem — An item switched on and off, with a check at its end while on. The menu stays open.
  • Menu.RadioGroup — Radio items of which one is chosen, held by value and onValueChange.
  • Menu.RadioItem — One choice in a radio group.
  • Menu.Sub — A submenu: its row and the list it opens.
  • Menu.SubTrigger — The row that opens a submenu, on hover or press, with a chevron at its end. end puts something before the chevron, such as a badge.
  • Menu.SubContent — The submenu's list, opened beside its row.
  • Menu.Group — Items that belong together. A screen reader announces them by their Menu.GroupLabel.
  • Menu.GroupLabel — The heading of a group.
  • Menu.Separator — A rule between groups.
  • Menu.Shortcut — The keys for an item, at its end. It only shows them; the keys have to be handled where the action lives.
  • Menu.Badge — A small count or flag at an item's end.
  • Menu.Description — What an item does, in quieter words after its label.

Examples

Basic

Items and nothing else. Pressing one closes the menu.

import { Button } from '@/components/panelui/button';
import { Menu } from '@/components/panelui/menu';

export default function MenuBasic() {
  return (
    <Menu>
      <Menu.Trigger render={<Button variant="outline" />}>Open</Menu.Trigger>
      <Menu.Content>
        <Menu.Item>Open in new tab</Menu.Item>
        <Menu.Item>Copy link</Menu.Item>
        <Menu.Item>Move to folder</Menu.Item>
      </Menu.Content>
    </Menu>
  );
}

Menu.Sub holds a Menu.SubTrigger row and the Menu.SubContent it opens to the side. Submenus nest.

import { Button } from '@/components/panelui/button';
import { Menu } from '@/components/panelui/menu';

export default function MenuSubmenu() {
  return (
    <Menu>
      <Menu.Trigger render={<Button variant="outline" />}>Open</Menu.Trigger>
      <Menu.Content>
        <Menu.Item>Rename</Menu.Item>
        <Menu.Sub>
          <Menu.SubTrigger>Move to</Menu.SubTrigger>
          <Menu.SubContent>
            <Menu.Item>Drafts</Menu.Item>
            <Menu.Item>Shared with me</Menu.Item>
            <Menu.Sub>
              <Menu.SubTrigger>Projects</Menu.SubTrigger>
              <Menu.SubContent>
                <Menu.Item>Spring launch</Menu.Item>
                <Menu.Item>Brand refresh</Menu.Item>
              </Menu.SubContent>
            </Menu.Sub>
          </Menu.SubContent>
        </Menu.Sub>
        <Menu.Item>Archive</Menu.Item>
      </Menu.Content>
    </Menu>
  );
}

Shortcuts

Menu.Shortcut puts the keys at the end of an item. It only shows them; handle the keys where the action lives.

import { Button } from '@/components/panelui/button';
import { Menu } from '@/components/panelui/menu';

export default function MenuShortcuts() {
  return (
    <Menu>
      <Menu.Trigger render={<Button variant="outline" />}>Edit</Menu.Trigger>
      <Menu.Content className="w-48">
        <Menu.Item>
          Undo
          <Menu.Shortcut>⌘Z</Menu.Shortcut>
        </Menu.Item>
        <Menu.Item>
          Redo
          <Menu.Shortcut>⇧⌘Z</Menu.Shortcut>
        </Menu.Item>
        <Menu.Separator />
        <Menu.Item>
          Cut
          <Menu.Shortcut>⌘X</Menu.Shortcut>
        </Menu.Item>
        <Menu.Item>
          Copy
          <Menu.Shortcut>⌘C</Menu.Shortcut>
        </Menu.Item>
        <Menu.Item>
          Paste
          <Menu.Shortcut>⌘V</Menu.Shortcut>
        </Menu.Item>
      </Menu.Content>
    </Menu>
  );
}

Icons

An icon written as an item's first child is sized and coloured by the row.

import { DownloadIcon, PencilIcon, PrinterIcon, Share2Icon } from 'lucide-react';
import { Button } from '@/components/panelui/button';
import { Menu } from '@/components/panelui/menu';

export default function MenuIcons() {
  return (
    <Menu>
      <Menu.Trigger render={<Button variant="outline" />}>Document</Menu.Trigger>
      <Menu.Content>
        <Menu.Item>
          <PencilIcon />
          Rename
        </Menu.Item>
        <Menu.Item>
          <Share2Icon />
          Share
        </Menu.Item>
        <Menu.Item>
          <DownloadIcon />
          Download
        </Menu.Item>
        <Menu.Item>
          <PrinterIcon />
          Print
        </Menu.Item>
      </Menu.Content>
    </Menu>
  );
}

Checkboxes

Menu.CheckboxItem switches on and off with a check at its end, and keeps the menu open so several can be changed in a row.

'use client';

import { Button } from '@/components/panelui/button';
import { Menu } from '@/components/panelui/menu';
import { useState } from 'react';

export default function MenuCheckboxes() {
  const [columns, setColumns] = useState({ owner: true, due: true, estimate: false });
  const toggle = (key: keyof typeof columns) => (on: boolean) => setColumns((current) => ({ ...current, [key]: on }));

  return (
    <Menu>
      <Menu.Trigger render={<Button variant="outline" />}>Columns</Menu.Trigger>
      <Menu.Content className="w-48">
        <Menu.Group>
          <Menu.GroupLabel>Show</Menu.GroupLabel>
          <Menu.CheckboxItem checked={columns.owner} onCheckedChange={toggle('owner')}>
            Owner
          </Menu.CheckboxItem>
          <Menu.CheckboxItem checked={columns.due} onCheckedChange={toggle('due')}>
            Due date
          </Menu.CheckboxItem>
          <Menu.CheckboxItem checked={columns.estimate} onCheckedChange={toggle('estimate')}>
            Estimate
          </Menu.CheckboxItem>
        </Menu.Group>
      </Menu.Content>
    </Menu>
  );
}

Radio group

Menu.RadioGroup holds one choice among its Menu.RadioItems, through value and onValueChange.

'use client';

import { Button } from '@/components/panelui/button';
import { Menu } from '@/components/panelui/menu';
import { useState } from 'react';

export default function MenuRadio() {
  const [density, setDensity] = useState('comfortable');

  return (
    <Menu>
      <Menu.Trigger render={<Button variant="outline" />}>Density</Menu.Trigger>
      <Menu.Content className="w-48">
        <Menu.Group>
          <Menu.GroupLabel>Row height</Menu.GroupLabel>
          <Menu.RadioGroup value={density} onValueChange={setDensity}>
            <Menu.RadioItem value="compact">Compact</Menu.RadioItem>
            <Menu.RadioItem value="comfortable">Comfortable</Menu.RadioItem>
            <Menu.RadioItem value="spacious">Spacious</Menu.RadioItem>
          </Menu.RadioGroup>
        </Menu.Group>
      </Menu.Content>
    </Menu>
  );
}

Destructive

variant="destructive" draws an item in the destructive colour, for an action that cannot be undone. Keep it last, after a separator.

import { ArchiveIcon, CopyIcon, PencilIcon, Trash2Icon } from 'lucide-react';
import { Button } from '@/components/panelui/button';
import { Menu } from '@/components/panelui/menu';

export default function MenuDestructive() {
  return (
    <Menu>
      <Menu.Trigger render={<Button variant="outline" />}>Project</Menu.Trigger>
      <Menu.Content>
        <Menu.Item>
          <PencilIcon />
          Rename
        </Menu.Item>
        <Menu.Item>
          <CopyIcon />
          Duplicate
        </Menu.Item>
        <Menu.Item>
          <ArchiveIcon />
          Archive
        </Menu.Item>
        <Menu.Separator />
        <Menu.Item variant="destructive">
          <Trash2Icon />
          Delete project
        </Menu.Item>
      </Menu.Content>
    </Menu>
  );
}

Descriptions and badges

Menu.Description adds quieter words after the label, and Menu.Badge a count or a flag at the end of the row.

import { BellIcon, CalendarIcon, FolderIcon, MessageSquareIcon, TriangleAlertIcon } from 'lucide-react';
import { Button } from '@/components/panelui/button';
import { Menu } from '@/components/panelui/menu';

export default function MenuDetails() {
  return (
    <Menu>
      <Menu.Trigger render={<Button variant="outline" />}>Connections</Menu.Trigger>
      <Menu.Content className="w-80">
        <Menu.Item>
          <CalendarIcon />
          Calendar
          <Menu.Description>Book and move meetings</Menu.Description>
        </Menu.Item>
        <Menu.Item>
          <FolderIcon />
          Shared drive
          <Menu.Description>Read your team's files</Menu.Description>
          <Menu.Badge>
            <TriangleAlertIcon />
            Sign in
          </Menu.Badge>
        </Menu.Item>
        <Menu.Item>
          <MessageSquareIcon />
          Chat
          <Menu.Description>Post to channels</Menu.Description>
        </Menu.Item>
        <Menu.Separator />
        <Menu.Item>
          <BellIcon />
          Notifications
          <Menu.Badge>3</Menu.Badge>
        </Menu.Item>
      </Menu.Content>
    </Menu>
  );
}

API Reference

PropTypeDefaultDescription
containerBaseMenu.Portal.Props['container']—Where in the document the menu is put. Default document.body.
anchorBaseMenu.Positioner.Props['anchor']—What it is placed against instead of the trigger: an element or a ref to one.
classNamestring—
PropTypeDefaultDescription
variant'default' | 'destructive'—destructive draws it in the destructive colour, for an action that cannot be undone. Default default.
classNamestring—
PropTypeDefaultDescription
classNamestring—
PropTypeDefaultDescription
classNamestring—
PropTypeDefaultDescription
classNamestring—
PropTypeDefaultDescription
classNamestring—
endReactNode—Something before the chevron, such as a Menu.Badge.
PropTypeDefaultDescription
sideBaseMenu.Positioner.Props['side']'bottom'Default inline-end: to the right in a left-to-right page.
PropTypeDefaultDescription
classNamestring—
PropTypeDefaultDescription
classNamestring—

Every part also takes the props of the element it renders, and a className.

Notes

Keyboard

With the trigger focused, Enter, Space or the down arrow opens the menu on its first item. The arrow keys move between items, typing jumps to one by its first letters, Enter presses it, and Escape closes the menu and puts focus back on the trigger. The right arrow opens a submenu and the left arrow closes it.

A submenu opens when its row is hovered, after a short delay, or pressed. The pointer can cross diagonally from the row to the submenu without closing it.

Closing

Pressing an item closes the menu; pressing a check or radio item does not. Pass closeOnClick to change either.

Server Components

Menu is a client component. In a file that starts with "use client", write its parts with a dot, as on this page. A Server Component cannot read Menu.Trigger off the import, so each part is also exported by name: MenuTrigger is Menu.Trigger, and so on for the others.

import { Menu, MenuTrigger, MenuContent, MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuSub, MenuSubTrigger, MenuSubContent, MenuGroup, MenuGroupLabel, MenuSeparator, MenuDescription, MenuShortcut, MenuBadge } from "@/components/panelui/menu"

On this page