Skip to content
Drivn
4 min read

Next.js Context Menu for the App Router

Add a right-click context menu to a Next.js App Router app. Drivn ships a single-file Context Menu that positions itself at the cursor with zero runtime deps.

A context menu is the panel that appears when a user right-clicks — the place for contextual actions like copy, rename, or delete, anchored to wherever the pointer was. Building one in Next.js means intercepting the browser's native contextmenu event, positioning a floating panel at the exact cursor coordinates, and dismissing it on the next click or Escape, all without shipping a positioning library into every route.

Drivn's Context Menu does this in one file you own. After npx drivn add context-menu the source lives in src/components/ui/context-menu.tsx, marked 'use client', with lucide-react as its only runtime dependency — no Radix, no floating-ui. Dot notation exposes every piece through a single import: ContextMenu.Trigger, ContextMenu.Content, ContextMenu.Item, ContextMenu.Sub, ContextMenu.SubTrigger, ContextMenu.SubContent, ContextMenu.Group, ContextMenu.Label, and ContextMenu.Separator. The right-click handler, cursor positioning, and dismiss logic are all wired inside the component.

This guide covers installing Drivn in a Next.js 16 project, rendering the menu as a client island, positioning it at the cursor with the contextmenu event, and composing submenus, shortcut hints, and destructive items. Every snippet is drawn from the component's actual source. For the full reference see the Context Menu docs; for the shadcn/ui comparison see Drivn vs shadcn/ui Context Menu.

Install in a Next.js 16 project

Drivn installs through a small CLI that writes the component source straight into your repository — there is no runtime npm package to version-lock. Open a terminal at the root of your Next.js 16 project and run npx drivn add context-menu. The CLI prompts once for your install directory (defaulting to src/components/ui/), copies context-menu.tsx, and adds lucide-react to your package.json for the ChevronRight icon the submenu trigger renders. The CLI reference documents every flag, including targeting a custom path or installing several components at once. After install you own the file outright — future Drivn releases never overwrite it, and you can edit the internals without forking a dependency. Commit it first to keep a clean baseline before customizing.

1# from the root of your Next.js 16 project
2npx drivn add context-menu

Render the menu as a client island

An App Router page is a server component by default, so it cannot run the right-click handler or hold open state. Drivn's Context Menu is marked 'use client' because the root component tracks open and the cursor position with useState and registers DOM listeners in a useEffect. The clean pattern is to keep the menu in a small client component and render that island inside an otherwise server-rendered page. Import ContextMenu from @/components/ui/context-menu, wrap your target in ContextMenu.Trigger, then list the actions inside ContextMenu.Content. The installation guide covers project bootstrapping; the Context Menu examples show the full set of patterns.

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

Position the menu at the cursor

The right-click positioning is the part most implementations get wrong, and Drivn handles it inside the root component. An onContextMenu handler calls e.preventDefault() to suppress the browser's native menu, stores e.clientX and e.clientY in a pos state object, and sets open to true. The Content component then reads that position and applies it as inline style={{ left: pos.x, top: pos.y }} on a fixed-positioned panel, so the menu appears exactly under the pointer. Dismissal is wired in a useEffect that, while open, listens for a mousedown anywhere to close and for the Escape key, removing both listeners on cleanup. The handler is verbatim from the component source below.

1// onContextMenu handler, verbatim from src/components/ui/context-menu.tsx
2const onContextMenu = React.useCallback(
3 (e: React.MouseEvent) => {
4 e.preventDefault()
5 setPos({ x: e.clientX, y: e.clientY })
6 setOpen(true)
7 }, [])

Add submenus, shortcuts, and destructive items

Nest a ContextMenu.Sub containing a ContextMenu.SubTrigger and ContextMenu.SubContent to build a flyout submenu — and it opens on hover with no extra JavaScript. The subContent styles use group-hover/sub:opacity-100 and group-hover/sub:visible against the parent's group/sub class, so CSS alone reveals the panel left-full top-0 beside its trigger. Pass a shortcut prop to any ContextMenu.Item to render a key hint pushed right with ml-auto text-xs text-muted-foreground, and add destructive to paint an item with text-destructive hover:bg-destructive/10 for delete actions. Use ContextMenu.Separator for a thin h-px bg-border divider and ContextMenu.Label for a muted group caption. See the Context Menu docs for every subcomponent and prop.

1<ContextMenu.Content>
2 <ContextMenu.Item icon={Pencil} shortcut="⌘E">
3 Edit
4 </ContextMenu.Item>
5 <ContextMenu.Sub>
6 <ContextMenu.SubTrigger icon={FolderOpen}>
7 Refactor
8 </ContextMenu.SubTrigger>
9 <ContextMenu.SubContent>
10 <ContextMenu.Item>Rename</ContextMenu.Item>
11 <ContextMenu.Item>Extract</ContextMenu.Item>
12 </ContextMenu.SubContent>
13 </ContextMenu.Sub>
14 <ContextMenu.Separator />
15 <ContextMenu.Item icon={Trash2} destructive shortcut="⌘⌫">
16 Delete
17 </ContextMenu.Item>
18</ContextMenu.Content>
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

The Context Menu itself is a client component — it is marked 'use client' because the root tracks the open state and cursor position with useState and registers mousedown and keydown listeners in a useEffect. Your surrounding page and layout still render on the server. The usual pattern is a small client component holding the menu and its trigger, rendered as an island inside an otherwise server-rendered route.

An onContextMenu handler on the trigger calls e.preventDefault() to block the browser menu, then stores e.clientX and e.clientY in a pos state object and opens the menu. The Content panel is fixed and applies that position as inline style={{ left: pos.x, top: pos.y }}, so it renders exactly where the pointer was when the user right-clicked.

No. The menu is built with plain React and Tailwind — no Radix, no floating-ui, no popper. Positioning comes from reading the native contextmenu event coordinates and applying them as inline styles on a fixed panel. Submenus open through CSS group-hover classes rather than JavaScript. The only runtime dependency the component adds is lucide-react for the ChevronRight icon on submenu triggers.

The submenu is driven entirely by CSS. The Sub wrapper carries a group/sub class, and the SubContent styles use group-hover/sub:opacity-100 and group-hover/sub:visible to reveal a panel positioned left-full top-0 beside its trigger. Because hover state is handled by Tailwind variants, no hover handlers or timers run in JavaScript — the flyout appears and hides purely through the cascade.