Skip to content
Drivn
5 min read

How to Add a Context Menu to a React App

Step-by-step guide to adding a copy-and-own right-click context menu to any React app with the Drivn CLI — cursor positioning, submenus, and shortcuts.

A context menu is the panel that appears on right-click — the place editors, file managers, and board apps put contextual actions like copy, rename, and delete, anchored to wherever the pointer was. Building one in React looks simple until you hit the real work: 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. Reach for a menu library and you inherit a portal, a positioning engine, and an API surface far larger than the feature needs.

Drivn answers it by writing the component into your own repository instead of installing a runtime package. The CLI copies a context-menu.tsx file with lucide-react as its only runtime dependency — no Radix, no floating-ui. A dot-notation API exposes every piece through one 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, so you compose menus rather than rebuild plumbing.

This guide adds the Context Menu to an existing React app — install the CLI, render a basic right-click menu, understand how it positions at the cursor, then add submenus, shortcut hints, and destructive items. It works the same in Vite + React or a Next.js App Router app. For Next.js notes see the Context Menu docs; for live patterns see the Context Menu examples.

Prerequisites

Before installing the Context Menu, confirm your React project has the three things Drivn assumes: Tailwind CSS v4 installed and processing your CSS, TypeScript configured (the component ships as a .tsx file), and a @/ path alias pointing at your source directory. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, all three are already wired. For a custom setup, check the compilerOptions.paths entry in tsconfig.json; the installation page lists the minimal config. The Context Menu pulls in only lucide-react for the ChevronRight icon its submenu trigger renders — there is no Radix, no floating-ui, and no portal dependency to install.

Step 1 — Install Drivn via the CLI

Run the CLI from your project root to add the Context Menu source file. The command prompts once for your install directory (defaulting to src/components/ui/), writes context-menu.tsx, and adds lucide-react to your package.json if it is not already present. No global config file is created — the Context Menu is a TypeScript file in your repo that you edit like any other component. Confirm the file landed in your editor, then commit it to keep a clean baseline before customizing. If your project uses a monorepo layout or a non-standard path, the CLI docs cover the flags for targeting a custom location during install.

1# add the Context Menu to your existing React project
2npx drivn add context-menu
3
4# verify the file was written
5ls src/components/ui/context-menu.tsx

Step 2 — Render a basic right-click menu

Import ContextMenu from your UI directory and compose the menu from its sub-components. Wrap the surface you want to make right-clickable in ContextMenu.Trigger, then list the actions inside ContextMenu.Content. Each ContextMenu.Item takes an icon prop as a component reference — icon={Copy}, not icon={<Copy />} — and renders the label as children. Because the root tracks open state and cursor position with useState, the file is marked 'use client'; in a Next.js App Router app, render it inside a client component. The Context Menu docs list the full prop table for every sub-component.

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}

Step 3 — How cursor positioning works

The part most implementations get wrong is positioning, and Drivn handles it inside the root component so you never touch it. 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 panel is fixed-positioned and applies that position as inline style={{ left: pos.x, top: pos.y }}, so it renders exactly under the pointer. Dismissal lives 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 below is verbatim from the component source — you read it, but you do not write it.

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 }, [])

Step 4 — Submenus, shortcuts, and groups

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 text-destructive hover:bg-destructive/10 for delete actions. Use ContextMenu.Group with a ContextMenu.Label for a captioned section and ContextMenu.Separator for a thin h-px bg-border divider. The examples page shows each pattern in full.

1<ContextMenu.Content>
2 <ContextMenu.Item icon={Scissors} shortcut="⌘X">
3 Cut
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 Component</ContextMenu.Item>
12 </ContextMenu.SubContent>
13 </ContextMenu.Sub>
14 <ContextMenu.Separator />
15 <ContextMenu.Item icon={Trash2} destructive>
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.

Follow Drivn updates
New components, improvements, and guides every release.
Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Yes. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add context-menu. The Context Menu has no dependency on Next.js or any router — it reads the native contextmenu event and positions a fixed panel, which works anywhere React reaches the DOM. Vite + React is the most common non-Next setup and works without extra configuration. The only package it adds is lucide-react for the submenu chevron icon.

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. You never compute coordinates yourself — the component does it.

No. The menu is built with plain React and Tailwind — no Radix, no floating-ui, no popper, and no portal. 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 timers. The only runtime dependency the component adds is lucide-react for the ChevronRight icon on submenu triggers.

It closes automatically. The root registers a mousedown listener in a useEffect while open, so any click — including the click that triggers a menu item — dismisses the panel. An Escape keydown listener closes it too. Both listeners are removed on cleanup. You only write the action handler on each ContextMenu.Item; the open and close lifecycle is managed inside the component.