Menu
A list of actions in a dropdown, enhanced with keyboard navigation. Built with Base UI and Tailwind CSS. Copy-paste ready.
import {
PauseIcon,
PlayIcon,
SkipBackIcon,
SkipForwardIcon,
TrashIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Menu,
MenuCheckboxItem,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuShortcut,
MenuSub,
MenuSubPopup,
MenuSubTrigger,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger render={<Button variant="outline" />}>Open menu</MenuTrigger>
<MenuPopup>
<MenuGroup>
<MenuGroupLabel>Playback</MenuGroupLabel>
<MenuItem>
<PlayIcon aria-hidden="true" />
Play
<MenuShortcut>⌘P</MenuShortcut>
</MenuItem>
<MenuItem disabled>
<PauseIcon aria-hidden="true" />
Pause
<MenuShortcut>⇧⌘P</MenuShortcut>
</MenuItem>
<MenuItem>
<SkipBackIcon aria-hidden="true" />
Previous
<MenuShortcut>⌘[</MenuShortcut>
</MenuItem>
<MenuItem>
<SkipForwardIcon aria-hidden="true" />
Next
<MenuShortcut>⌘]</MenuShortcut>
</MenuItem>
</MenuGroup>
<MenuSeparator />
<MenuCheckboxItem>Shuffle</MenuCheckboxItem>
<MenuCheckboxItem>Repeat</MenuCheckboxItem>
<MenuCheckboxItem disabled>Enhanced Audio</MenuCheckboxItem>
<MenuSeparator />
<MenuGroup>
<MenuGroupLabel>Sort by</MenuGroupLabel>
<MenuRadioGroup>
<MenuRadioItem value="artist">Artist</MenuRadioItem>
<MenuRadioItem value="album">Album</MenuRadioItem>
<MenuRadioItem value="title">Title</MenuRadioItem>
</MenuRadioGroup>
</MenuGroup>
<MenuSeparator />
<MenuCheckboxItem variant="switch">Auto save</MenuCheckboxItem>
<MenuSeparator />
<MenuSub>
<MenuSubTrigger>Add to Playlist</MenuSubTrigger>
<MenuSubPopup>
<MenuItem>Jazz</MenuItem>
<MenuSub>
<MenuSubTrigger>Rock</MenuSubTrigger>
<MenuSubPopup>
<MenuItem>Hard Rock</MenuItem>
<MenuItem>Soft Rock</MenuItem>
<MenuItem>Classic Rock</MenuItem>
<MenuSeparator />
<MenuItem>Metal</MenuItem>
<MenuItem>Punk</MenuItem>
<MenuItem>Grunge</MenuItem>
<MenuItem>Alternative</MenuItem>
<MenuItem>Indie</MenuItem>
<MenuItem>Electronic</MenuItem>
</MenuSubPopup>
</MenuSub>
<MenuItem>Pop</MenuItem>
</MenuSubPopup>
</MenuSub>
<MenuSeparator />
<MenuItem variant="destructive">
<TrashIcon aria-hidden="true" />
Delete
<MenuShortcut>⌘⌫</MenuShortcut>
</MenuItem>
</MenuPopup>
</Menu>
);
}
Installation
pnpm dlx shadcn@latest add @coss/menu
Usage
import {
Menu,
MenuCheckboxItem,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuSub,
MenuSubPopup,
MenuSubTrigger,
MenuTrigger,
} from "@/components/ui/menu"<Menu>
<MenuTrigger>Open</MenuTrigger>
<MenuPopup align="start" sideOffset={4}>
<MenuItem>Profile</MenuItem>
<MenuSeparator />
<MenuGroup>
<MenuGroupLabel>Playback</MenuGroupLabel>
<MenuItem>Play</MenuItem>
<MenuItem>Pause</MenuItem>
</MenuGroup>
<MenuSeparator />
<MenuCheckboxItem>Shuffle</MenuCheckboxItem>
<MenuCheckboxItem>Repeat</MenuCheckboxItem>
<MenuCheckboxItem variant="switch">Auto save</MenuCheckboxItem>
<MenuSeparator />
<MenuGroup>
<MenuGroupLabel>Sort by</MenuGroupLabel>
<MenuRadioGroup>
<MenuRadioItem>Artist</MenuRadioItem>
<MenuRadioItem>Album</MenuRadioItem>
<MenuRadioItem>Title</MenuRadioItem>
</MenuRadioGroup>
</MenuGroup>
<MenuSeparator />
<MenuSub>
<MenuSubTrigger>Add to playlist</MenuSubTrigger>
<MenuSubPopup>
<MenuItem>Jazz</MenuItem>
<MenuItem>Rock</MenuItem>
</MenuSubPopup>
</MenuSub>
</MenuPopup>
</Menu>Examples
Open on Hover
The menu popup opens when the trigger is hovered rather than clicked, useful for navigation bars and toolbars.
import { Button } from "@/components/ui/button";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger openOnHover render={<Button variant="outline" />}>
Hover me
</MenuTrigger>
<MenuPopup>
<MenuItem>Item one</MenuItem>
<MenuItem>Item two</MenuItem>
</MenuPopup>
</Menu>
);
}
With Checkbox
Uses MenuCheckboxItem to let users toggle individual options on or off within a persistent menu.
import { Button } from "@/components/ui/button";
import {
Menu,
MenuCheckboxItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger render={<Button variant="outline" />}>Open menu</MenuTrigger>
<MenuPopup>
<MenuCheckboxItem defaultChecked>Auto save</MenuCheckboxItem>
<MenuCheckboxItem>Notifications</MenuCheckboxItem>
</MenuPopup>
</Menu>
);
}
With Switch
MenuCheckboxItem supports a variant="switch" prop that displays a decorative switch indicator instead of a checkmark. This is a purely visual variant - the component remains a checkbox item with the same functionality.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPopup,
DialogTitle,
} from "@/components/ui/dialog";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<>
<Menu>
<MenuTrigger render={<Button variant="outline" />}>
Open menu
</MenuTrigger>
<MenuPopup align="start">
<MenuItem onClick={() => setDialogOpen(true)}>Open dialog</MenuItem>
</MenuPopup>
</Menu>
<Dialog onOpenChange={setDialogOpen} open={dialogOpen}>
<DialogPopup>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>Change your preferences</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>Close</DialogClose>
</DialogFooter>
</DialogPopup>
</Dialog>
</>
);
}
With Radio Group
Uses MenuRadioGroup and MenuRadioItem to enforce single selection among a set of mutually exclusive options.
import { Button } from "@/components/ui/button";
import {
Menu,
MenuCheckboxItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger render={<Button variant="outline" />}>Open menu</MenuTrigger>
<MenuPopup>
<MenuCheckboxItem defaultChecked variant="switch">
Auto save
</MenuCheckboxItem>
<MenuCheckboxItem variant="switch">Notifications</MenuCheckboxItem>
<MenuCheckboxItem defaultChecked variant="switch">
Dark mode
</MenuCheckboxItem>
</MenuPopup>
</Menu>
);
}
With Link
Renders MenuItem elements as anchor tags for navigating to routes directly from the menu popup.
import { Button } from "@/components/ui/button";
import {
Menu,
MenuPopup,
MenuRadioGroup,
MenuRadioItem,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger render={<Button variant="outline" />}>Open menu</MenuTrigger>
<MenuPopup>
<MenuRadioGroup defaultValue="system">
<MenuRadioItem value="light">Light</MenuRadioItem>
<MenuRadioItem value="dark">Dark</MenuRadioItem>
<MenuRadioItem value="system">System</MenuRadioItem>
</MenuRadioGroup>
</MenuPopup>
</Menu>
);
}
With Group Label
Organizes menu items into labeled sections using MenuGroup and MenuGroupLabel for better scanability.
import Link from "next/link";
import { Button } from "@/components/ui/button";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger render={<Button variant="outline" />}>Open menu</MenuTrigger>
<MenuPopup>
<MenuItem render={<Link href="/docs" />}>Docs</MenuItem>
<MenuItem render={<Link href="/particles" />}>Particles</MenuItem>
</MenuPopup>
</Menu>
);
}
Nested Menu
Opens a MenuSub popup from a parent item to reveal a secondary list of options without closing the parent menu.
import { Button } from "@/components/ui/button";
import {
Menu,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuSeparator,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger render={<Button variant="outline" />}>Open menu</MenuTrigger>
<MenuPopup>
<MenuGroup>
<MenuGroupLabel>Account</MenuGroupLabel>
<MenuItem>Profile</MenuItem>
<MenuItem>Billing</MenuItem>
</MenuGroup>
<MenuSeparator />
<MenuGroup>
<MenuGroupLabel>Support</MenuGroupLabel>
<MenuItem>Docs</MenuItem>
<MenuItem>Contact</MenuItem>
</MenuGroup>
</MenuPopup>
</Menu>
);
}
Close on Click
Demonstrates controlling closeOnClick behavior so the menu dismisses immediately when an item is selected.
import { Button } from "@/components/ui/button";
import {
Menu,
MenuItem,
MenuPopup,
MenuSub,
MenuSubPopup,
MenuSubTrigger,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger render={<Button variant="outline" />}>Open menu</MenuTrigger>
<MenuPopup>
<MenuItem>Item one</MenuItem>
<MenuSub>
<MenuSubTrigger>More</MenuSubTrigger>
<MenuSubPopup>
<MenuItem>Sub item A</MenuItem>
<MenuItem>Sub item B</MenuItem>
</MenuSubPopup>
</MenuSub>
<MenuItem>Item two</MenuItem>
</MenuPopup>
</Menu>
);
}
Open a Dialog
Triggers a Dialog from a MenuItem, allowing secondary confirmation flows to be launched directly from a menu.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPopup,
DialogTitle,
} from "@/components/ui/dialog";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<>
<Menu>
<MenuTrigger render={<Button variant="outline" />}>
Open menu
</MenuTrigger>
<MenuPopup align="start">
<MenuItem onClick={() => setDialogOpen(true)}>Open dialog</MenuItem>
</MenuPopup>
</Menu>
<Dialog onOpenChange={setDialogOpen} open={dialogOpen}>
<DialogPopup>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>Change your preferences</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>Close</DialogClose>
</DialogFooter>
</DialogPopup>
</Dialog>
</>
);
}
Account Menu
A circular avatar trigger with user info in the MenuGroupLabel header, account links, and a destructive "Sign out" item at the bottom.
import {
CreditCardIcon,
LogOutIcon,
SettingsIcon,
UserIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Menu,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuSeparator,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger
render={
<Button
aria-label="Open account menu"
className="size-9 rounded-full font-semibold"
variant="outline"
/>
}
>
JS
</MenuTrigger>
<MenuPopup align="end">
<MenuGroup>
<MenuGroupLabel>
<div className="flex flex-col gap-0.5">
<span>Jane Smith</span>
<span className="font-normal text-muted-foreground text-xs">
jane@example.com
</span>
</div>
</MenuGroupLabel>
</MenuGroup>
<MenuSeparator />
<MenuItem>
<UserIcon aria-hidden="true" />
Profile
</MenuItem>
<MenuItem>
<CreditCardIcon aria-hidden="true" />
Billing
</MenuItem>
<MenuItem>
<SettingsIcon aria-hidden="true" />
Settings
</MenuItem>
<MenuSeparator />
<MenuItem variant="destructive">
<LogOutIcon aria-hidden="true" />
Sign out
</MenuItem>
</MenuPopup>
</Menu>
);
}
File Actions
A labeled "File actions" trigger grouping rename, duplicate, move, and download items with keyboard shortcuts, plus a destructive delete at the bottom.
import {
CopyIcon,
DownloadIcon,
FilePenIcon,
MoveIcon,
TrashIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Menu,
MenuGroup,
MenuItem,
MenuPopup,
MenuSeparator,
MenuShortcut,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger render={<Button variant="outline" />}>
File actions
</MenuTrigger>
<MenuPopup align="start">
<MenuGroup>
<MenuItem>
<FilePenIcon aria-hidden="true" />
Rename
<MenuShortcut>F2</MenuShortcut>
</MenuItem>
<MenuItem>
<CopyIcon aria-hidden="true" />
Duplicate
<MenuShortcut>⌘D</MenuShortcut>
</MenuItem>
<MenuItem>
<MoveIcon aria-hidden="true" />
Move to
</MenuItem>
<MenuItem>
<DownloadIcon aria-hidden="true" />
Download
<MenuShortcut>⌘↓</MenuShortcut>
</MenuItem>
</MenuGroup>
<MenuSeparator />
<MenuItem variant="destructive">
<TrashIcon aria-hidden="true" />
Delete
<MenuShortcut>⌫</MenuShortcut>
</MenuItem>
</MenuPopup>
</Menu>
);
}
Icon Button Trigger
A compact size="icon" ghost button (⋮) as the trigger — a common pattern for per-row action menus in lists and tables.
import {
ArchiveIcon,
EllipsisVerticalIcon,
FlagIcon,
ShareIcon,
StarIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Menu,
MenuItem,
MenuPopup,
MenuSeparator,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger
render={
<Button aria-label="More options" size="icon" variant="ghost" />
}
>
<EllipsisVerticalIcon aria-hidden="true" className="size-4" />
</MenuTrigger>
<MenuPopup align="end">
<MenuItem>
<StarIcon aria-hidden="true" />
Add to favourites
</MenuItem>
<MenuItem>
<ShareIcon aria-hidden="true" />
Share
</MenuItem>
<MenuItem>
<ArchiveIcon aria-hidden="true" />
Archive
</MenuItem>
<MenuSeparator />
<MenuItem variant="destructive">
<FlagIcon aria-hidden="true" />
Report
</MenuItem>
</MenuPopup>
</Menu>
);
}
Notification Settings
A bell icon trigger that opens a menu mixing variant="switch" channel toggles with standard checkbox activity options in separate groups.
import { BellIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Menu,
MenuCheckboxItem,
MenuGroup,
MenuGroupLabel,
MenuPopup,
MenuSeparator,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger
render={
<Button
aria-label="Notification settings"
size="icon"
variant="outline"
/>
}
>
<BellIcon aria-hidden="true" className="size-4" />
</MenuTrigger>
<MenuPopup align="end">
<MenuGroup>
<MenuGroupLabel>Channels</MenuGroupLabel>
<MenuCheckboxItem defaultChecked variant="switch">
Email
</MenuCheckboxItem>
<MenuCheckboxItem defaultChecked variant="switch">
Push
</MenuCheckboxItem>
<MenuCheckboxItem variant="switch">SMS</MenuCheckboxItem>
</MenuGroup>
<MenuSeparator />
<MenuGroup>
<MenuGroupLabel>Activity</MenuGroupLabel>
<MenuCheckboxItem defaultChecked>Mentions</MenuCheckboxItem>
<MenuCheckboxItem defaultChecked>Comments</MenuCheckboxItem>
<MenuCheckboxItem>Reactions</MenuCheckboxItem>
</MenuGroup>
</MenuPopup>
</Menu>
);
}
Share with Export Sub-menu
A "Share" button trigger offering a copy link item alongside an "Export as" MenuSub with format choices (PDF, PNG, SVG, CSV).
import { DownloadIcon, ExternalLinkIcon, ShareIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Menu,
MenuItem,
MenuPopup,
MenuSeparator,
MenuSub,
MenuSubPopup,
MenuSubTrigger,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Menu>
<MenuTrigger render={<Button variant="outline" />}>
<ShareIcon aria-hidden="true" />
Share
</MenuTrigger>
<MenuPopup align="start">
<MenuItem>
<ExternalLinkIcon aria-hidden="true" />
Copy link
</MenuItem>
<MenuSub>
<MenuSubTrigger>
<DownloadIcon aria-hidden="true" />
Export as
</MenuSubTrigger>
<MenuSubPopup>
<MenuItem>PDF</MenuItem>
<MenuItem>PNG</MenuItem>
<MenuItem>SVG</MenuItem>
<MenuSeparator />
<MenuItem>CSV (data only)</MenuItem>
</MenuSubPopup>
</MenuSub>
</MenuPopup>
</Menu>
);
}

