webdevpuneet.com

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

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

CategoryTables

Copy & paste

Source Code

<div class="rcm-wrap">
  <div class="rcm-bar">
    <h3>Projects</h3>
    <span class="rcm-hint">Right-click a row for actions</span>
  </div>
  <table class="rcm-table">
    <thead><tr><th>Name</th><th>Owner</th><th>Status</th></tr></thead>
    <tbody id="rcmBody"></tbody>
  </table>
</div>
<div class="rcm-menu" id="rcmMenu" hidden role="menu">
  <button type="button" class="rcm-item" data-action="edit" role="menuitem">
    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
    Edit
  </button>
  <button type="button" class="rcm-item" data-action="duplicate" role="menuitem">
    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
    Duplicate
  </button>
  <div class="rcm-sep"></div>
  <button type="button" class="rcm-item rcm-danger" data-action="delete" role="menuitem">
    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
    Delete
  </button>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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.

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
Offer quick row actions on right-click, with preventDefault() suppressing the browser's generic menu in favour of your own.
Project and task boards
Edit, duplicate or delete tasks from a menu opened at the exact cursor position using clientX and clientY.
CRM contact tables
Provide quick actions per contact, pairing with a selectable table and a bulk actions bar for multi-row operations.
Spreadsheet-like tools
Mirror native spreadsheet right-click behaviour, clamping menu coordinates so it never renders off-screen near viewport edges.
Content libraries and context menu learning
Duplicate or archive entries in an asset list with a data table, splicing the real ROWS array so actions genuinely change the data.

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.