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.jsonpnpm dlx shadcn@latest add https://panelui.dev/r/web/menu.jsonyarn dlx shadcn@latest add https://panelui.dev/r/web/menu.jsonbunx shadcn@latest add https://panelui.dev/r/web/menu.jsonThe source is copied into components/panelui/, where you can edit it.
npm install panelui-webpnpm add panelui-webyarn add panelui-webbun add panelui-webImport 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-variantspnpm add @base-ui/react tailwind-variantsyarn add @base-ui/react tailwind-variantsbun add @base-ui/react tailwind-variantsCopy 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.renderdraws it as another element, such as aButton.Menu.Content— The list, in a layer over the page.sideandalignplace it against the trigger, and it moves to the other side when there is no room.anchorplaces 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 byvalueandonValueChange.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.endputs 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 theirMenu.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>
);
}Submenu
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
Menu.Content
| Prop | Type | Default | Description |
|---|---|---|---|
container | BaseMenu.Portal.Props['container'] | — | Where in the document the menu is put. Default document.body. |
anchor | BaseMenu.Positioner.Props['anchor'] | — | What it is placed against instead of the trigger: an element or a ref to one. |
className | string | — |
Menu.Item
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'default' | 'destructive' | — | destructive draws it in the destructive colour, for an action that cannot be undone. Default default. |
className | string | — |
Menu.CheckboxItem
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — |
Menu.RadioGroup
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — |
Menu.RadioItem
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — |
Menu.SubTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
end | ReactNode | — | Something before the chevron, such as a Menu.Badge. |
Menu.SubContent
| Prop | Type | Default | Description |
|---|---|---|---|
side | BaseMenu.Positioner.Props['side'] | 'bottom' | Default inline-end: to the right in a left-to-right page. |
Menu.Group
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — |
Menu.GroupLabel
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — |
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.
Submenus
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"