Context Menu
Displays a menu to the user — such as a set of actions or functions — triggered by a button. Built with Base UI and Tailwind CSS. Copy-paste ready.
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export function ContextMenuDemo() {
return (
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click here
</span>
<span className="pointer-coarse:inline-block hidden">
Long press here
</span>
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuGroup>
<ContextMenuItem>
Back
<ContextMenuShortcut>⌘[</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem disabled>
Forward
<ContextMenuShortcut>⌘]</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Reload
<ContextMenuShortcut>⌘R</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>More Tools</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-44">
<ContextMenuGroup>
<ContextMenuItem>Save Page...</ContextMenuItem>
<ContextMenuItem>Create Shortcut...</ContextMenuItem>
<ContextMenuItem>Name Window...</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem>Developer Tools</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem variant="destructive">Delete</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuSubContent>
</ContextMenuSub>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuCheckboxItem checked>
Show Bookmarks
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem>Show Full URLs</ContextMenuCheckboxItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuRadioGroup value="pedro">
<ContextMenuLabel>People</ContextMenuLabel>
<ContextMenuRadioItem value="pedro">
Pedro Duarte
</ContextMenuRadioItem>
<ContextMenuRadioItem value="colm">Colm Tuite</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/context-menu
Examples
Submenu
Demonstrates a nested submenu that opens from a parent item, revealing a secondary list of options without closing the parent.
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export function ContextMenuSubmenu() {
return (
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click here
</span>
<span className="pointer-coarse:inline-block hidden">
Long press here
</span>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuItem>
Copy
<ContextMenuShortcut>⌘C</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Cut
<ContextMenuShortcut>⌘X</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSub>
<ContextMenuSubTrigger>More Tools</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuGroup>
<ContextMenuItem>Save Page...</ContextMenuItem>
<ContextMenuItem>Create Shortcut...</ContextMenuItem>
<ContextMenuItem>Name Window...</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem>Developer Tools</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem variant="destructive">Delete</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuSubContent>
</ContextMenuSub>
</ContextMenuContent>
</ContextMenu>
);
}
Shortcuts
Each menu item shows a keyboard shortcut hint aligned to the right of its label using CommandShortcut.
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export function ContextMenuShortcuts() {
return (
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click here
</span>
<span className="pointer-coarse:inline-block hidden">
Long press here
</span>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuItem>
Back
<ContextMenuShortcut>⌘[</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem disabled>
Forward
<ContextMenuShortcut>⌘]</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Reload
<ContextMenuShortcut>⌘R</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem>
Save
<ContextMenuShortcut>⌘S</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Save As...
<ContextMenuShortcut>⇧⌘S</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
);
}
Group
Items are organized into named groups with visual separators using MenuGroup and MenuGroupLabel.
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export function ContextMenuGroups() {
return (
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click here
</span>
<span className="pointer-coarse:inline-block hidden">
Long press here
</span>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuLabel>File</ContextMenuLabel>
<ContextMenuItem>
New File
<ContextMenuShortcut>⌘N</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Open File
<ContextMenuShortcut>⌘O</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Save
<ContextMenuShortcut>⌘S</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuLabel>Edit</ContextMenuLabel>
<ContextMenuItem>
Undo
<ContextMenuShortcut>⌘Z</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Redo
<ContextMenuShortcut>⇧⌘Z</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem>
Cut
<ContextMenuShortcut>⌘X</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Copy
<ContextMenuShortcut>⌘C</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Paste
<ContextMenuShortcut>⌘V</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem variant="destructive">
Delete
<ContextMenuShortcut>⌫</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
);
}
Icons
Each menu item includes a leading icon to aid quick visual scanning of the available actions.
import {
ClipboardPasteIcon,
CopyIcon,
ScissorsIcon,
TrashIcon,
} from "lucide-react";
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export function ContextMenuIcons() {
return (
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click here
</span>
<span className="pointer-coarse:inline-block hidden">
Long press here
</span>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuItem>
<CopyIcon />
Copy
</ContextMenuItem>
<ContextMenuItem>
<ScissorsIcon />
Cut
</ContextMenuItem>
<ContextMenuItem>
<ClipboardPasteIcon />
Paste
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem variant="destructive">
<TrashIcon />
Delete
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
);
}
Checkboxes
Menu items can be toggled on or off using MenuCheckboxItem, persisting their checked state across menu opens.
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export function ContextMenuCheckboxes() {
return (
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click here
</span>
<span className="pointer-coarse:inline-block hidden">
Long press here
</span>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuCheckboxItem defaultChecked>
Show Bookmarks Bar
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem>Show Full URLs</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem defaultChecked>
Show Developer Tools
</ContextMenuCheckboxItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
);
}
Radio
A MenuRadioGroup inside the menu enforces single selection among a set of mutually exclusive options.
"use client";
import * as React from "react";
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export function ContextMenuRadio() {
const [user, setUser] = React.useState("pedro");
const [theme, setTheme] = React.useState("light");
return (
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click here
</span>
<span className="pointer-coarse:inline-block hidden">
Long press here
</span>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuLabel>People</ContextMenuLabel>
<ContextMenuRadioGroup onValueChange={setUser} value={user}>
<ContextMenuRadioItem value="pedro">
Pedro Duarte
</ContextMenuRadioItem>
<ContextMenuRadioItem value="colm">Colm Tuite</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuLabel>Theme</ContextMenuLabel>
<ContextMenuRadioGroup onValueChange={setTheme} value={theme}>
<ContextMenuRadioItem value="light">Light</ContextMenuRadioItem>
<ContextMenuRadioItem value="dark">Dark</ContextMenuRadioItem>
<ContextMenuRadioItem value="system">System</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
);
}
Destructive
Shows a destructive variant item styled in red for irreversible actions such as delete or remove.
import { PencilIcon, ShareIcon, TrashIcon } from "lucide-react";
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export function ContextMenuDestructive() {
return (
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click here
</span>
<span className="pointer-coarse:inline-block hidden">
Long press here
</span>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuItem>
<PencilIcon />
Edit
</ContextMenuItem>
<ContextMenuItem>
<ShareIcon />
Share
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem variant="destructive">
<TrashIcon />
Delete
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
);
}
Sides
Demonstrates the available alignment positions and side placements for the context menu popup relative to its trigger.
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export function ContextMenuSides() {
return (
<div className="grid w-full max-w-sm grid-cols-2 gap-4">
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click (top)
</span>
<span className="pointer-coarse:inline-block hidden">
Long press (top)
</span>
</ContextMenuTrigger>
<ContextMenuContent side="top">
<ContextMenuGroup>
<ContextMenuItem>Back</ContextMenuItem>
<ContextMenuItem>Forward</ContextMenuItem>
<ContextMenuItem>Reload</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click (right)
</span>
<span className="pointer-coarse:inline-block hidden">
Long press (right)
</span>
</ContextMenuTrigger>
<ContextMenuContent side="right">
<ContextMenuGroup>
<ContextMenuItem>Back</ContextMenuItem>
<ContextMenuItem>Forward</ContextMenuItem>
<ContextMenuItem>Reload</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click (bottom)
</span>
<span className="pointer-coarse:inline-block hidden">
Long press (bottom)
</span>
</ContextMenuTrigger>
<ContextMenuContent side="bottom">
<ContextMenuGroup>
<ContextMenuItem>Back</ContextMenuItem>
<ContextMenuItem>Forward</ContextMenuItem>
<ContextMenuItem>Reload</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span className="pointer-fine:inline-block hidden">
Right click (left)
</span>
<span className="pointer-coarse:inline-block hidden">
Long press (left)
</span>
</ContextMenuTrigger>
<ContextMenuContent side="left">
<ContextMenuGroup>
<ContextMenuItem>Back</ContextMenuItem>
<ContextMenuItem>Forward</ContextMenuItem>
<ContextMenuItem>Reload</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
</div>
);
}
Inside Dialog
Opens the context menu within a dialog to demonstrate correct overlay stacking and focus management.
import { ClipboardIcon, CopyIcon, ScissorsIcon, TrashIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function Pattern() {
return (
<div className="flex w-full items-center justify-center p-12">
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Open Dialog
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Context Menu Example</DialogTitle>
<DialogDescription>
Right click on the area below to see the context menu.
</DialogDescription>
</DialogHeader>
<ContextMenu>
<ContextMenuTrigger className="flex aspect-[2/0.5] w-full items-center justify-center rounded-lg border border-dashed text-muted-foreground text-sm">
Right click here
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuItem>
<CopyIcon />
Copy
</ContextMenuItem>
<ContextMenuItem>
<ScissorsIcon />
Cut
</ContextMenuItem>
<ContextMenuItem>
<ClipboardIcon />
Paste
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>More Options</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuGroup>
<ContextMenuItem>Save Page...</ContextMenuItem>
<ContextMenuItem>Create Shortcut...</ContextMenuItem>
<ContextMenuItem>Name Window...</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem>Developer Tools</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem variant="destructive">
<TrashIcon />
Delete
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
</DialogContent>
</Dialog>
</div>
);
}
Bookmark List
A list of bookmark rows where right-clicking reveals open, copy link, star/unstar, share, duplicate, and destructive delete actions.
"use client";
import {
CopyIcon,
EditIcon,
ExternalLinkIcon,
LinkIcon,
ShareIcon,
StarIcon,
TrashIcon,
} from "lucide-react";
import { useState } from "react";
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
const LINKS = [
{ href: "#", label: "Design Systems 101" },
{ href: "#", label: "Component Architecture" },
{ href: "#", label: "Accessibility Guide" },
{ href: "#", label: "Tailwind Best Practices" },
];
export function Pattern() {
const [starred, setStarred] = useState<Set<string>>(new Set());
return (
<div className="w-full max-w-xs space-y-1 rounded-xl border p-2">
{LINKS.map((link) => (
<ContextMenu key={link.label}>
<ContextMenuTrigger className="flex w-full cursor-default items-center justify-between rounded-md px-3 py-2 text-sm hover:bg-muted">
<span>{link.label}</span>
{starred.has(link.label) && (
<StarIcon className="size-3.5 fill-amber-400 text-amber-400" />
)}
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuGroup>
<ContextMenuItem>
<ExternalLinkIcon />
Open
</ContextMenuItem>
<ContextMenuItem>
<LinkIcon />
Copy link
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem>
<EditIcon />
Rename
</ContextMenuItem>
<ContextMenuItem
onSelect={() =>
setStarred((prev) => {
const next = new Set(prev);
next.has(link.label)
? next.delete(link.label)
: next.add(link.label);
return next;
})
}
>
<StarIcon />
{starred.has(link.label) ? "Unstar" : "Star"}
</ContextMenuItem>
<ContextMenuItem>
<ShareIcon />
Share
</ContextMenuItem>
<ContextMenuItem>
<CopyIcon />
Duplicate
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem variant="destructive">
<TrashIcon />
Delete
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
))}
</div>
);
}
Rich Text Formatting
Right-clicking a text area reveals Bold, Italic, and Underline checkbox items that toggle styles live, plus an alignment submenu and an insert-link action.
Right-click to format this text sample.
"use client";
import {
AlignCenterIcon,
AlignLeftIcon,
AlignRightIcon,
BoldIcon,
ItalicIcon,
LinkIcon,
UnderlineIcon,
} from "lucide-react";
import { useState } from "react";
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
export function Pattern() {
const [bold, setBold] = useState(false);
const [italic, setItalic] = useState(false);
const [underline, setUnderline] = useState(false);
const [align, setAlign] = useState<"left" | "center" | "right">("left");
return (
<ContextMenu>
<ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed">
<p
className={`max-w-[160px] text-muted-foreground text-sm text-${align} ${bold ? "font-bold" : ""} ${italic ? "italic" : ""} ${underline ? "underline" : ""}`}
>
Right-click to format this text sample.
</p>
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuLabel>Formatting</ContextMenuLabel>
<ContextMenuGroup>
<ContextMenuCheckboxItem checked={bold} onCheckedChange={setBold}>
<BoldIcon />
Bold
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem checked={italic} onCheckedChange={setItalic}>
<ItalicIcon />
Italic
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem
checked={underline}
onCheckedChange={setUnderline}
>
<UnderlineIcon />
Underline
</ContextMenuCheckboxItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>
<AlignLeftIcon />
Alignment
</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuGroup>
<ContextMenuItem onSelect={() => setAlign("left")}>
<AlignLeftIcon />
Left
</ContextMenuItem>
<ContextMenuItem onSelect={() => setAlign("center")}>
<AlignCenterIcon />
Center
</ContextMenuItem>
<ContextMenuItem onSelect={() => setAlign("right")}>
<AlignRightIcon />
Right
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem>
<LinkIcon />
Insert link
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
);
}
File Explorer
A file list with per-row extension badges. Right-clicking any row opens open, rename, download, get-info, and delete actions for that specific file.
"use client";
import {
DownloadIcon,
FolderOpenIcon,
InfoIcon,
PencilIcon,
RefreshCwIcon,
TrashIcon,
} from "lucide-react";
import { useState } from "react";
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
type FileItem = {
ext: string;
id: string;
modified: string;
name: string;
size: string;
};
const FILES: FileItem[] = [
{
ext: "tsx",
id: "f1",
modified: "2m ago",
name: "button.tsx",
size: "4.2 KB",
},
{ ext: "ts", id: "f2", modified: "1h ago", name: "utils.ts", size: "1.8 KB" },
{
ext: "mdx",
id: "f3",
modified: "3h ago",
name: "readme.mdx",
size: "8.6 KB",
},
{
ext: "json",
id: "f4",
modified: "1d ago",
name: "package.json",
size: "2.1 KB",
},
];
const extColor: Record<string, string> = {
json: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-400",
mdx: "bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-400",
ts: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400",
tsx: "bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-400",
};
export function Pattern() {
const [selected, setSelected] = useState<string | null>(null);
return (
<div className="w-full max-w-sm space-y-0.5 rounded-xl border p-1">
{FILES.map((file) => (
<ContextMenu key={file.id}>
<ContextMenuTrigger
className={`flex w-full cursor-default items-center justify-between rounded-md px-3 py-2 text-sm transition-colors hover:bg-muted ${selected === file.id ? "bg-muted" : ""}`}
onClick={() => setSelected(file.id)}
>
<div className="flex items-center gap-2.5">
<span
className={`rounded px-1.5 py-0.5 font-mono font-semibold text-[10px] ${extColor[file.ext] ?? "bg-muted text-muted-foreground"}`}
>
.{file.ext}
</span>
<span>{file.name}</span>
</div>
<span className="text-muted-foreground text-xs">
{file.modified}
</span>
</ContextMenuTrigger>
<ContextMenuContent className="w-44">
<ContextMenuGroup>
<ContextMenuItem>
<FolderOpenIcon />
Open
</ContextMenuItem>
<ContextMenuItem>
<PencilIcon />
Rename
</ContextMenuItem>
<ContextMenuItem>
<DownloadIcon />
Download
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem>
<InfoIcon />
<span>
Get info
<span className="ml-auto text-muted-foreground text-xs">
{file.size}
</span>
</span>
</ContextMenuItem>
<ContextMenuItem>
<RefreshCwIcon />
Refresh
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem variant="destructive">
<TrashIcon />
Delete
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
))}
</div>
);
}
Social Feed
Post cards with upvote/downvote controls that update the count live. A Report submenu with reason options and a mute-author destructive action complete the menu.
Just shipped a redesign of our onboarding flow. Cut drop-off by 34% in the first week. The key was reducing the number of steps from 7 to 3.
Hot take: dark mode is not an accessibility feature — it's a preference. True accessibility is about contrast ratios and text size, not color scheme.
"use client";
import {
ArrowUpIcon,
FlagIcon,
MessageSquareIcon,
ThumbsDownIcon,
ThumbsUpIcon,
UserXIcon,
} from "lucide-react";
import { useState } from "react";
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
const POSTS = [
{
author: "Luna V.",
body: "Just shipped a redesign of our onboarding flow. Cut drop-off by 34% in the first week. The key was reducing the number of steps from 7 to 3.",
id: "p1",
time: "2h",
},
{
author: "Rafi A.",
body: "Hot take: dark mode is not an accessibility feature — it's a preference. True accessibility is about contrast ratios and text size, not color scheme.",
id: "p2",
time: "5h",
},
];
export function Pattern() {
const [votes, setVotes] = useState<Record<string, number>>({
p1: 24,
p2: 11,
});
return (
<div className="w-full max-w-sm space-y-3">
{POSTS.map((post) => (
<ContextMenu key={post.id}>
<ContextMenuTrigger className="block w-full cursor-default rounded-xl border px-4 py-3 text-left">
<div className="mb-2 flex items-center justify-between">
<span className="font-semibold text-sm">{post.author}</span>
<span className="text-muted-foreground text-xs">
{post.time} ago
</span>
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
{post.body}
</p>
<div className="mt-3 flex items-center gap-3 text-muted-foreground text-xs">
<span className="flex items-center gap-1">
<ArrowUpIcon className="size-3.5" />
{votes[post.id]}
</span>
<span className="flex items-center gap-1">
<MessageSquareIcon className="size-3.5" />
Reply
</span>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-44">
<ContextMenuGroup>
<ContextMenuItem
onSelect={() =>
setVotes((v) => ({ ...v, [post.id]: (v[post.id] ?? 0) + 1 }))
}
>
<ThumbsUpIcon />
Upvote
</ContextMenuItem>
<ContextMenuItem
onSelect={() =>
setVotes((v) => ({
...v,
[post.id]: Math.max(0, (v[post.id] ?? 0) - 1),
}))
}
>
<ThumbsDownIcon />
Downvote
</ContextMenuItem>
<ContextMenuItem>
<MessageSquareIcon />
Reply
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>
<FlagIcon />
Report
</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuGroup>
<ContextMenuItem>Spam</ContextMenuItem>
<ContextMenuItem>Misinformation</ContextMenuItem>
<ContextMenuItem>Harassment</ContextMenuItem>
<ContextMenuItem>Off-topic</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuGroup>
<ContextMenuItem variant="destructive">
<UserXIcon />
Mute author
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
))}
</div>
);
}
Task Board
Task rows with priority badges and status icons. The context menu exposes a status radio group, a priority submenu, reassign, archive, and delete — all updating the row in place.
"use client";
import {
ArchiveIcon,
CheckCircle2Icon,
CircleIcon,
ClockIcon,
TagIcon,
TrashIcon,
UserIcon,
} from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
type Status = "todo" | "in-progress" | "done";
type Task = {
assignee: string;
id: string;
priority: "high" | "medium" | "low";
status: Status;
title: string;
};
const INITIAL: Task[] = [
{
assignee: "AK",
id: "t1",
priority: "high",
status: "in-progress",
title: "Refactor auth middleware",
},
{
assignee: "SR",
id: "t2",
priority: "medium",
status: "todo",
title: "Write API documentation",
},
{
assignee: "MN",
id: "t3",
priority: "low",
status: "done",
title: "Update README",
},
];
const statusIcon: Record<Status, React.ReactNode> = {
done: <CheckCircle2Icon className="size-3.5 text-emerald-500" />,
"in-progress": <ClockIcon className="size-3.5 text-blue-500" />,
todo: <CircleIcon className="size-3.5 text-muted-foreground" />,
};
const priorityVariant: Record<
Task["priority"],
"destructive" | "warning" | "secondary"
> = {
high: "destructive",
low: "secondary",
medium: "warning",
};
export function Pattern() {
const [tasks, setTasks] = useState<Task[]>(INITIAL);
const updateTask = (id: string, patch: Partial<Task>) =>
setTasks((prev) => prev.map((t) => (t.id === id ? { ...t, ...patch } : t)));
const removeTask = (id: string) =>
setTasks((prev) => prev.filter((t) => t.id !== id));
return (
<div className="w-full max-w-sm space-y-1 rounded-xl border p-1">
{tasks.map((task) => (
<ContextMenu key={task.id}>
<ContextMenuTrigger className="flex w-full cursor-default items-center gap-3 rounded-md px-3 py-2.5 text-sm hover:bg-muted">
{statusIcon[task.status]}
<span className="flex-1">{task.title}</span>
<div className="flex items-center gap-2">
<Badge size="sm" variant={priorityVariant[task.priority]}>
{task.priority}
</Badge>
<span className="flex size-6 items-center justify-center rounded-full bg-muted font-semibold text-[10px]">
{task.assignee}
</span>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuLabel>Status</ContextMenuLabel>
<ContextMenuRadioGroup
onValueChange={(v) =>
updateTask(task.id, { status: v as Status })
}
value={task.status}
>
<ContextMenuRadioItem value="todo">To Do</ContextMenuRadioItem>
<ContextMenuRadioItem value="in-progress">
In Progress
</ContextMenuRadioItem>
<ContextMenuRadioItem value="done">Done</ContextMenuRadioItem>
</ContextMenuRadioGroup>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>
<TagIcon />
Priority
</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuRadioGroup
onValueChange={(v) =>
updateTask(task.id, { priority: v as Task["priority"] })
}
value={task.priority}
>
<ContextMenuRadioItem value="high">High</ContextMenuRadioItem>
<ContextMenuRadioItem value="medium">
Medium
</ContextMenuRadioItem>
<ContextMenuRadioItem value="low">Low</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuGroup>
<ContextMenuItem>
<UserIcon />
Reassign
</ContextMenuItem>
<ContextMenuItem>
<ArchiveIcon />
Archive
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem
onSelect={() => removeTask(task.id)}
variant="destructive"
>
<TrashIcon />
Delete
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
))}
</div>
);
}

