More Navigation Snippets
Context Menu — Free HTML CSS JS Right-Click Snippet
Context Menu · Navigation · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Context Menu — contextmenu Event, Viewport Clamping, Pop-in Animation & Three Close Methods

A context menu is the floating menu that appears when a user right-clicks (or long-presses on touch). It surfaces contextual actions relevant to the specific element that was right-clicked — Copy, Paste, Rename, Delete — without consuming any permanent screen space, like a click-triggered dropdown menu. Context menus are a staple of desktop-class web interfaces: file managers, data tables, spreadsheet editors, image editors, map applications, and any interface that aims to match native application interactions.
The contextmenu event and preventDefault
The document.addEventListener('contextmenu', showMenu) listener intercepts every right-click on the stage area. e.preventDefault() is the critical line — without it, the browser's built-in context menu appears on top of the custom one. After prevention, e.clientX and e.clientY give the cursor position relative to the viewport, which becomes the menu's top-left position.
Viewport edge clamping
A context menu positioned directly at clientX, clientY will overflow the right or bottom edge when the user right-clicks near a viewport boundary. The clamping calculation prevents this: const x = Math.min(e.clientX, window.innerWidth - 180) — 180 is the menu's minimum width. If the cursor is within 180px of the right edge, the menu is shifted left to fit. Similarly for Y: Math.min(e.clientY, window.innerHeight - 240) — 240 is the approximate menu height. This calculation is a common source of bugs in context menu implementations that skip it and only discover the issue during testing near edges.
The pop-in animation from the click point
The menu has default CSS transform: scale(0.95); opacity: 0; pointer-events: none; transition: opacity 0.12s, transform 0.12s. Adding .show transitions to transform: scale(1); opacity: 1; pointer-events: all. The transform-origin: top left anchors the scale animation to the menu's top-left corner — which is closest to the cursor position — making the menu appear to grow outward from the click point. This feels more natural than a centre-origin scale or a slide animation.
Three dismissal mechanisms
Well-implemented context menus have three ways to close: clicking a menu item (which performs an action then dismisses), clicking anywhere outside the menu (the standard dismiss gesture), and pressing the Escape key (keyboard-accessible dismiss). The document.addEventListener('click', ...) listener removes .show on any click. The document.addEventListener('keydown', e => { if (e.key === 'Escape') ... }) handles keyboard dismiss. Each menu item's onclick handler calls menu.classList.remove('show') after performing its action.
Danger item styling
The Delete item has a .danger class that applies color: #dc2626 (red) and background: #fef2f2 on hover, following the universal convention that destructive actions are shown in red to warn users before they click.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to work out the edge-clamping math yourself to see why it matters. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the menu's x and y positions are each computed with Math.min against the viewport dimensions minus a fixed offset, and what visibly breaks if that clamping is removed and you right-click near a corner. The same assistant can help optimize it — for instance asking whether the hardcoded 180 and 240 pixel estimates should instead be measured from the actual rendered menu size via getBoundingClientRect after a first invisible render. It's also useful for extending the menu: ask it to support nested submenus that flip direction near the screen edge, add keyboard arrow-key navigation between menu items once it's open, or show a different set of items depending on which specific element was right-clicked using event delegation. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a custom right-click context menu in plain HTML, CSS, and JavaScript — no menu library, no frameworks.
Requirements:
- Listen for the contextmenu event on a container, call preventDefault on it so the browser's native right-click menu never appears, and use the event's clientX/clientY as the basis for the custom menu's position.
- Before applying that position, clamp both the x and y coordinates so the menu can never render partially off-screen: cap x to the viewport width minus the menu's width and y to the viewport height minus the menu's height, so right-clicking near any edge or corner still produces a fully visible menu.
- The menu must be hidden by default via opacity 0, a slightly-scaled-down transform, and pointer-events disabled, then transition to full opacity, scale 1, and enabled pointer-events when a visibility class is added — with transform-origin set to the corner nearest the click point so it visibly grows outward from where the user clicked rather than from its geometric center.
- Include at least one divider element between groups of menu items, and mark one item (e.g. a delete/destructive action) with distinct styling to visually flag it as dangerous.
- Provide three independent ways to close the menu: clicking any menu item (after performing its action), clicking anywhere else in the document, and pressing the Escape key — all of which must remove the same visibility class.
- Structure the item click handlers so that swapping the placeholder action (e.g. an alert) for real per-item logic requires touching only that one handler, not the positioning or dismissal code.Step by step
How to Use
- 1Right-click anywhere in the dashed stage areaRight-click in the stage area to open the context menu at the cursor position. The menu pops in with a scale + opacity animation from its top-left corner. Click a menu item, click outside, or press Escape to close it. Try right-clicking near the edge of the preview to see the viewport clamping in action.
- 2Click a menu item to see the action and dismissClick any menu item (Copy, Cut, Paste, Rename, Share, Delete) to trigger an alert with the item name and automatically dismiss the menu. In your implementation, replace the alert() call inside the action() function with your actual handler logic for each item.
- 3Add, remove, or reorder menu items in the HTMLIn the HTML panel, each <li> inside .ctx-menu is a menu item. Add new items by copying an existing li and updating the SVG icon and text. The .divider class on a li creates a horizontal separator line. The .danger class on the Delete item applies the red destructive-action styling.
- 4Bind to a specific element instead of the whole documentIn the JS panel, change the contextmenu listener target from the stage element's oncontextmenu attribute to a specific element: myElement.addEventListener('contextmenu', showMenu). This shows the menu only when right-clicking that specific element. Use event delegation with e.target.closest() for dynamic elements in lists or tables.
- 5Add a submenu to a menu itemAdd a nested .ctx-menu ul as a child of a li item. In CSS, set the nested menu to display: none by default and display: block on .has-submenu:hover .ctx-menu. Position it with left: 100%; top: 0 to open to the right of the parent item. Add the viewport clamping logic to detect when the submenu would overflow the right edge and flip it to left: -100%.
- 6Export as HTML, JSX, or Tailwind for your projectClick HTML for a standalone file ready to use in any web project, JSX for a React ContextMenu component with onContextMenu, x, y, and items props, or Tailwind for a Tailwind CSS version. The JSX export manages visibility via useState and attaches close listeners via useEffect with cleanup on unmount.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The showMenu function calculates the clamped position before setting the menu's left and top: const x = Math.min(e.clientX, window.innerWidth - 180) prevents the menu from overflowing the right edge by capping the left position so the full 180px-wide menu fits. const y = Math.min(e.clientY, window.innerHeight - 240) prevents bottom overflow for the approximate 240px menu height. Both values use Math.min — if the cursor position leaves enough room, the menu appears at the cursor; if not, it is shifted inward just enough to fit. This ensures the menu is always fully visible regardless of where in the viewport the user right-clicks.
The .ctx-menu element has transform: scale(0.95); opacity: 0 in its default state, combined with transition: opacity 0.12s, transform 0.12s and pointer-events: none. Adding the .show class changes these to transform: scale(1); opacity: 1; pointer-events: all, triggering the CSS transition. The transform-origin: top left is critical — it anchors the scale animation to the top-left corner of the menu, which is the corner closest to the cursor (since the menu opens to the right and below the click point). Without this, the menu would scale from its geometric center, creating a floating animation that doesn't feel attached to the click position.
Use event delegation with e.target.closest() to detect which element type was right-clicked. In the showMenu function: const fileEl = e.target.closest('[data-type="file"]'); const rowEl = e.target.closest('[data-type="row"]'); Then render different menu items based on which element matched. An elegant pattern is to store menu item definitions per type in an object: const menuItems = { file: [{label:'Copy',...},{label:'Delete',...}], row: [{label:'Edit',...},...] }. Pass the matched type to a renderMenu() function that populates the ul from the correct items array.
The contextmenu event does not fire consistently on touch devices. For mobile context menus, add a touchstart listener that starts a 500ms timer: const timer = setTimeout(() => showMenu(touch.clientX, touch.clientY), 500). In touchend and touchmove listeners, call clearTimeout(timer) to cancel if the user lifts their finger or moves before 500ms. On iOS, also call e.preventDefault() in touchstart to prevent the browser's default callout menu from appearing. This pattern reliably detects long-press and is the standard technique for touch-based contextual menus.