Table Row Context Menu — Real Right-Click contextmenu Handling (JS)

Table Row Right-Click Context Menu · Tables · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Real contextmenu interception
e.preventDefault() on the native event genuinely suppresses the browser menu.
Cursor-positioned menu
Opens exactly at clientX/clientY, not a fixed corner.
Viewport-clamped placement
Coordinates are clamped so the menu never renders off-screen near an edge.
Genuine data mutation
Edit/Duplicate/Delete splice and update the real ROWS array, then re-render.
Active row highlight
The targeted row is visually marked while its menu is open.
Multiple dismissal paths
Closes on outside click, Escape, a new right-click, or scroll.
No stacked menus
Right-clicking elsewhere closes any open menu before deciding whether to reopen.
Data-driven & no library
Renders from a ROWS array of objects with generic id-based lookups.

About this UI Snippet

Table Row Context Menu — Right-Click a Row for a Real Custom Menu

Screenshot of the Table Row Right-Click Context Menu snippet rendered live

Desktop apps use right-click context menus constantly; most web tables ignore the gesture entirely and let the browser's generic menu appear instead. This snippet intercepts it properly: right-clicking a table row suppresses the native browser menu and opens a real custom one, positioned at the cursor, with Edit, Duplicate, and Delete actions that genuinely mutate the row data and re-render — all in plain HTML, CSS, and vanilla JavaScript.

Intercepting the real contextmenu event

The table body listens for the native contextmenu event and calls e.preventDefault() the moment it fires on a row — this is the one line that actually suppresses the browser's own right-click menu; without it, both menus would fight for the same click. Because it's the genuine contextmenu event (not a click-and-hold hack or a visible "⋮" button), the interaction feels native: right-click anywhere on the row, not just a specific icon, opens the menu.

Positioned at the cursor, clamped to the viewport

openMenu() reads e.clientX/e.clientY and sets the menu's position: fixed coordinates directly to the click point, so it opens exactly where the user right-clicked — then clamps both axes against window.innerWidth/innerHeight so a right-click near the edge of the screen never renders the menu partially off-screen, which is what a naive "always position at cursor" implementation gets wrong.

Real mutations, not decorative buttons

Each menu action operates on the actual ROWS array: Edit prompts for a new name and writes it back to the matching record by id; Duplicate splices a cloned row (with a fresh id) directly after the original; Delete splices the row out entirely. Every action is followed by render(), which rebuilds the table body from the mutated array — so the table you see after clicking Delete has genuinely lost that row from the underlying data, not just visually hidden it.

Closes the way users expect

The menu closes on an outside click, on Escape, on right-clicking elsewhere (so a new right-click doesn't stack a second menu on top), and on scroll (since a fixed-position menu would otherwise drift away from the row it belongs to). This set of dismissal paths is what separates a context menu that feels reliable from one that leaves stray panels floating on screen.

Visual feedback on the target row

The row under the cursor gets an rcm-active highlight while its menu is open, so it's unambiguous which row Edit/Duplicate/Delete will affect — important once a menu is positioned near the cursor rather than anchored to the row itself. Pair this with a selectable table for multi-row bulk actions, or a row detail panel for a left-click drill-down alongside this right-click menu.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than debugging why a homemade right-click menu doesn't suppress the browser's own menu, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why e.preventDefault() must run inside the contextmenu event handler itself (not some other event) to stop the native menu, and how openMenu() clamps its position against window.innerWidth/innerHeight so the menu never renders off-screen near a viewport edge. The same assistant can help extend it — ask it to add keyboard navigation between menu items with arrow keys and Enter to activate, support a different set of actions depending on the row's status field, or add a confirmation step before Delete fires. 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:

text
Build a table where right-clicking a row opens a real custom context menu, in plain HTML, CSS, and JavaScript — no libraries.

Requirements:
- Attach a listener for the genuine native contextmenu event on the table body (not a click-and-hold simulation or a visible button), and call e.preventDefault() inside that handler as soon as a row is identified as the target, so the browser's own right-click menu is genuinely suppressed rather than appearing alongside your custom one.
- On a valid right-click, show a custom menu element positioned with fixed positioning at the exact cursor coordinates (clientX/clientY) from the event — then clamp the computed left/top values against the viewport's width and height (accounting for the menu's own measured dimensions) so the menu never renders partially or fully off-screen when right-clicking near an edge or corner.
- The menu must offer at least three actions — Edit, Duplicate, Delete — and each action must operate on a real underlying JavaScript array of row objects (found by matching the target row's id), not just update the DOM directly: Edit should update a field's value in that array element, Duplicate should insert a cloned object (with a new unique id) into the array, and Delete should remove the object from the array. After any action, re-render the entire table body from the mutated array so the visible table reflects a genuine data change.
- Visually highlight the specific row that the currently-open menu targets, so it's unambiguous which row an action will affect.
- Close the menu on: a click anywhere outside the menu element, pressing the Escape key, and right-clicking on a different row or empty space (without letting two context menus stack on top of each other).

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA projects table renders; the custom menu markup starts hidden.
  2. 2
    Right-click a rowThe browser's native menu is suppressed; a custom menu opens at your cursor.
  3. 3
    Choose EditA prompt lets you rename the row; the table re-renders with the change.
  4. 4
    Choose DuplicateA cloned row is inserted directly below the original.
  5. 5
    Choose DeleteThe row is removed from the underlying data and the table.
  6. 6
    Dismiss the menuClick outside, press Escape, right-click elsewhere, or scroll — all close it.

Real-world uses

Common Use Cases

Admin and file managers
Right-click rows for quick actions, similar to a desktop file explorer — pair with a selectable table for multi-select.
Project and task boards
Edit, duplicate, or delete tasks without leaving the list view.
CRM and contact tables
Offer quick actions per contact next to a data table.
Spreadsheet-like tools
Mirror native spreadsheet right-click behavior for rows.
Content and asset libraries
Duplicate or archive entries via a familiar right-click gesture.
Learning contextmenu handling
A reference for real contextmenu interception — compare with a bulk actions bar for toolbar-driven actions instead.

Got questions?

Frequently Asked Questions

Yes. The table body listens for the real contextmenu event and calls e.preventDefault() as soon as it fires on a row, which is the standard, correct way to stop the browser's own menu from appearing. Without that call, both the native menu and the custom one would try to show simultaneously.

Yes. Each action operates on the real in-memory ROWS array by finding the row's index by id — Edit overwrites its name field, Duplicate splices in a cloned object with a new id, and Delete splices the row out entirely. render() is called afterward to rebuild the table from the mutated array, so the visible change reflects an actual data change, not a CSS hide.

openMenu() clamps the computed left/top position against window.innerWidth and window.innerHeight minus the menu's own measured width and height, so a right-click near the right or bottom edge of the viewport still renders the full menu on-screen instead of clipping off the edge.

Four things: clicking anywhere outside the menu, pressing Escape, right-clicking on a different part of the page (which closes any open menu before deciding whether to open a new one), and scrolling the page (since a fixed-position menu would otherwise drift away from the row it targets).

Keep the menu's open state, position, and target row id in component state; attach an onContextMenu (React) or @contextmenu.prevent (Vue) handler to each row that calls preventDefault and sets that state. The clamping math and the mutate-then-re-render pattern are framework-agnostic — only the state management moves into the framework.