Skip to content
Drivn
5 min read

React Context Menu Component Examples

Drop-in React Context Menu examples: right-click menu with icons and shortcuts, nested submenus, destructive items, grouped sections, and disabled state.

Right-click is where an app puts the actions that would clutter a toolbar — rename, duplicate, move to folder, delete. The Context Menu component in Drivn covers that surface with React state and CSS, nothing else. No Radix, no portal, no floating-ui: the panel is a fixed div placed at the clientX / clientY the onContextMenu event reports, and two document listeners close it on outside mousedown or Escape.

Every snippet on this page imports from @/components/ui/context-menu — the exact path npx drivn add context-menu writes to — so they compile in a Next.js or Vite project with no edits. The component ships as typed .tsx source, so props like icon, shortcut, and destructive autocomplete at the call site. Once the file lands in your repo it is yours, which matters more here than for most components: menus are the thing every product ends up bending to its own shape.

Five patterns follow, ordered roughly by how often they show up in real apps: a basic menu with icons, shortcut hints on the right edge of each row, hover-opened submenus, destructive and disabled states, and labeled groups. Take the closest one and swap the items for yours. For the shadcn/ui side-by-side, see Drivn vs shadcn Context Menu.

Basic right-click menu with icons

The default Context Menu wraps any region with a Trigger and renders the menu list inside Content. Right-click anywhere inside the trigger and the menu opens at the cursor; click outside or press Escape and it closes. Pass an icon prop on Item and the source renders it via React.isValidElement(Icon) ? Icon : <Icon className="w-4 h-4" />, so you can hand it either a component reference (icon={Copy}) or a fully styled element (icon={<Copy className="w-4 h-4 text-blue-500" />}). The structure mirrors the Context Menu docs usage and matches what npx drivn add context-menu writes into your project.

1import { ContextMenu } from "@/components/ui/context-menu"
2import { Copy, Trash2 } from "lucide-react"
3
4export default function Page() {
5 return (
6 <ContextMenu>
7 <ContextMenu.Trigger>
8 <div>Right-click here</div>
9 </ContextMenu.Trigger>
10 <ContextMenu.Content>
11 <ContextMenu.Item icon={Copy}>
12 Copy
13 </ContextMenu.Item>
14 <ContextMenu.Item icon={Trash2} destructive>
15 Delete
16 </ContextMenu.Item>
17 </ContextMenu.Content>
18 </ContextMenu>
19 )
20}

Items with keyboard shortcut hints

The shortcut prop on Item renders a right-aligned muted span — the source applies ml-auto text-xs text-muted-foreground so the hint sits opposite the label inside the same row. The text is purely visual; there is no key handling attached. Pair it with the actual hotkey logic in your app — a useEffect that listens for keydown and dispatches the same handler the menu item triggers. The shortcut string supports any glyph including the macOS Command symbol ⌘, Control ⌃, Shift ⇧, and Option ⌥. The example below renders ⌘X, ⌘C, and ⌘V next to Cut, Copy, and Paste — the canonical clipboard set most editor menus expose.

1<ContextMenu>
2 <ContextMenu.Trigger>
3 <div>Right-click here</div>
4 </ContextMenu.Trigger>
5 <ContextMenu.Content>
6 <ContextMenu.Item icon={Scissors} shortcut="⌘X">
7 Cut
8 </ContextMenu.Item>
9 <ContextMenu.Item icon={Copy} shortcut="⌘C">
10 Copy
11 </ContextMenu.Item>
12 <ContextMenu.Item icon={Clipboard} shortcut="⌘V">
13 Paste
14 </ContextMenu.Item>
15 </ContextMenu.Content>
16</ContextMenu>

Nested submenu

Submenus open on hover, and the whole mechanism is a pair of Tailwind classes. Sub renders relative group/sub. SubContent starts at opacity-0 invisible and carries group-hover/sub:opacity-100 plus group-hover/sub:visible, so hovering the parent row flips both at once. The panel itself sits at absolute left-full top-0 with min-w-[180px], which lands it flush against the right edge of the parent menu. No timer, no click handler, no extra state — a transition-[opacity,visibility] duration-150 handles the fade, easing in by default and switching to ease-out on hover via group-hover/sub:ease-out.

SubTrigger appends <ChevronRight className="ml-auto w-4 h-4" /> after its children, so the expandable affordance is automatic and you never pass the chevron yourself. It also accepts the same icon prop as Item, which is how the example below puts a FolderOpen glyph on the Refactor row. Nesting another Sub inside a SubContent repeats the same two classes one level down — see the Context Menu docs for the full subcomponent list.

1<ContextMenu>
2 <ContextMenu.Trigger>
3 <div>Right-click here</div>
4 </ContextMenu.Trigger>
5 <ContextMenu.Content>
6 <ContextMenu.Item icon={Pencil}>
7 Edit
8 </ContextMenu.Item>
9 <ContextMenu.Sub>
10 <ContextMenu.SubTrigger icon={FolderOpen}>
11 Refactor
12 </ContextMenu.SubTrigger>
13 <ContextMenu.SubContent>
14 <ContextMenu.Item>
15 Rename
16 </ContextMenu.Item>
17 <ContextMenu.Item>
18 Extract Component
19 </ContextMenu.Item>
20 <ContextMenu.Item>
21 Move to Folder
22 </ContextMenu.Item>
23 </ContextMenu.SubContent>
24 </ContextMenu.Sub>
25 </ContextMenu.Content>
26</ContextMenu>

Destructive and disabled items

Two boolean props handle the most common item states. destructive swaps in red-tinted styles via text-destructive hover:bg-destructive/10 — the right look for irreversible actions like Delete or Remove. disabled applies opacity-50 pointer-events-none and forwards the disabled attribute to the underlying <button>, so the row neither reacts to hover nor fires onClick. Both flags compose with icon and shortcut so a destructive shortcut item like ⌘⌫ Delete reads the way users expect. The styles object lives at the top of the source file — see the Context Menu docs if you want to add a third state like "warning" by extending styles and threading a new prop through Item.

1<ContextMenu>
2 <ContextMenu.Trigger>
3 <div>Right-click here</div>
4 </ContextMenu.Trigger>
5 <ContextMenu.Content>
6 <ContextMenu.Item icon={Star}>
7 Add to favorites
8 </ContextMenu.Item>
9 <ContextMenu.Item icon={Archive} disabled>
10 Archive
11 </ContextMenu.Item>
12 <ContextMenu.Separator />
13 <ContextMenu.Item icon={Trash2} destructive shortcut="⌘⌫">
14 Delete
15 </ContextMenu.Item>
16 </ContextMenu.Content>
17</ContextMenu>

Grouped sections with labels

Group and Label partition the menu into themed sections. Label renders a small uppercase-feel header via px-3 py-1.5 text-xs font-medium text-muted-foreground — visually distinct from items but inside the same flow. Group wraps related items with py-1 so adjacent groups breathe without needing a hard Separator. Use this pattern when the menu carries enough actions that scanning by category becomes faster than reading top to bottom — for a notes app you might split History (Undo, Redo) from Format (Bold, Italic) from Actions (Share, Delete). After the basic installation, the same pattern works for both inline and submenu variants.

1<ContextMenu>
2 <ContextMenu.Trigger>
3 <div>Right-click here</div>
4 </ContextMenu.Trigger>
5 <ContextMenu.Content>
6 <ContextMenu.Group>
7 <ContextMenu.Label>Actions</ContextMenu.Label>
8 <ContextMenu.Item icon={Undo2} shortcut="⌘Z">
9 Undo
10 </ContextMenu.Item>
11 <ContextMenu.Item icon={Redo2} shortcut="⌘⇧Z">
12 Redo
13 </ContextMenu.Item>
14 </ContextMenu.Group>
15 </ContextMenu.Content>
16</ContextMenu>
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Yes. The component imports React, the ChevronRight icon from lucide-react, and the local cn utility — nothing else. State lives in a small Context provider, the menu is a fixed-positioned div placed at the cursor coordinates from the onContextMenu event, and submenus open via the group-hover/sub CSS selector. There is no portal layer and no floating-ui, so the entire feature ships in roughly two hundred lines of TSX you own after install.

The ContextMenuRoot component already handles it. The internal onContextMenu callback calls e.preventDefault() before opening the Drivn menu, so the native browser menu never appears inside the trigger region. Anywhere outside the trigger, the browser default still works — that is the right behavior, since Drivn should not hijack right-click globally. Wrap only the regions where you want the custom menu to take over.

Not in the current implementation. Drivn's submenu opens via group-hover/sub CSS, which means the panel becomes visible while the cursor is over the parent Sub. Click-to-open submenus would require additional state per submenu and a click handler on SubTrigger. If your app needs that pattern, the Context Menu file is small enough to extend — store an open state on the Sub, toggle it from SubTrigger, and replace the group-hover/sub: classes with a conditional class on SubContent.

Yes. The Item component's onClick handler calls your onClick prop and then calls setOpen(false) from the Context Menu context. Both fire in sequence, so the action runs before the menu unmounts. If you need to keep the menu open after a click — for a multi-select pattern, for example — wrap the action in a custom item that does not close the menu, or render a checkbox row inside Item and stop propagation on the inner change handler.

Drivn does not include automatic edge-collision detection. The menu renders at { left: pos.x, top: pos.y } from the click coordinates, so right-clicking near the bottom-right corner of the page can clip the menu. For most app shells this is fine because triggers are inside content regions with margin to spare. If you need clipping protection, you can override the Content className with style overrides that read window.innerWidth and clamp the position, or wrap the whole component in a useEffect that reads getBoundingClientRect after open and adjusts.