Engineering Journal
Table Formatter
Table Formatter

Building a Right-Click Context Menu That Knows What You Clicked

2026-05-14

TLDR

Attaching individual contextmenu listeners to every tab or accordion button increases memory usage and breaks when adding dynamic elements. Utilizing a single delegated contextmenu listener on the parent container alongside e.target.closest() dynamically dispatches action sets based on element types while clamping menu coordinates within viewport bounds.
Event Listener StrategyMemory FootprintDynamic Element HandlingViewport Boundary Handling
Per-Element BindingHigh (grows with tab count)Requires re-binding on element addManual calculation per element
Delegated Container BindingMinimal (Single parent listener)100% Automatic via closest()Centralized clientX/Y Clamping

Why individual context listeners cause event handler bloat

When UI bars support multiple interactive element types, such as individual sheet tabs (.sp-option) and expandable section headers (button.accordion), binding separate context menu listeners to each node creates unnecessary event overhead.

Elements added dynamically also require explicit listener attachment, increasing code complexity and edge-case bug risks.


Centralized event delegation and viewport clamping

We attached a single contextmenu listener to the tab bar container, using e.target.closest() to identify target element types and position a fixed floating menu:

// Delegated right-click context menu handler
export function bindTabContextMenu(containerEl, menuEl) {
  containerEl.addEventListener('contextmenu', (e) => {
    e.preventDefault();

const tab = e.target.closest('.sp-option'); const accordion = e.target.closest('button.accordion'); const target = tab || accordion; if (!target) return;

const targetType = tab ? 'sheet' : 'section'; renderContextMenuItems(menuEl, target, targetType); positionFixedMenu(menuEl, e.clientX, e.clientY); }); }

function positionFixedMenu(menuEl, clientX, clientY) { menuEl.style.position = 'fixed'; menuEl.style.left = ${clientX}px; menuEl.style.top = ${clientY}px; menuEl.style.display = 'block';

// Clamp within viewport bounds to prevent off-screen overflow const rect = menuEl.getBoundingClientRect(); if (rect.right > window.innerWidth) menuEl.style.left = ${clientX - rect.width}px; if (rect.bottom > window.innerHeight) menuEl.style.top = ${clientY - rect.height}px; }

Dismissal listeners attached to click and scroll events ensure the floating menu closes cleanly when users interact elsewhere on the page.

Rule of thumb: Delegate context menu events to parent containers using e.target.closest() and clamp positions using clientX/clientY.
Read this post in the full Engineering Journal →