Context Menu
Context menu triggered on right-click, positioned exactly at the cursor and automatically flipped when it would overflow the container's edges.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Navigation has 36 effects, including 4 free. Explore the category →
3 usage examples



How it works
The contextmenu event is captured with preventDefault() to suppress the browser's native menu. The opening position is computed in container-relative coordinates: clientX - rect.left / clientY - rect.top — the menu opens exactly where the cursor is, regardless of the page's scroll position.
The menu is first moved off-screen (left: -9999px) so the browser renders it as visible but imperceptible. A requestAnimationFrame then waits for the browser to compute its actual dimensions via getBoundingClientRect(). This one-frame delay is the prerequisite for knowing the menu's exact width and height before placing it definitively.
The edge-flip logic compares position + width (and height) of the menu against the container's dimensions. If the menu would overflow to the right, it is shifted to containerWidth - menuWidth - 8 px; same for the bottom. A floor at 4 px prevents any overflow to the left or top.
The .open class triggers the appearance via a CSS transition (opacity 0→1, scale 0.95→1, 150 ms). Closing fires on any outside click (document-level click listener) or the Escape key.
Accessibility
- prefers-reduced-motion not handled: the appearance transition (
opacity + scale, 150 ms) applies to all users, including those with "reduce motion" enabled in their OS. Add@media (prefers-reduced-motion: reduce) { .ctx-menu { transition: none } }if needed. - Menu items are native
<button>elements — keyboard-focusable and correctly announced by screen readers. - No ARIA
menu/menuitemroles are applied, and noaria-expandedis set on the trigger element — full ARIA context-menu semantics are not implemented in this code. - Closing via
Escapeis implemented through akeydownlistener ondocument. - Keyboard shortcut hints (
Ctrl+C,Del…) displayed in items are decorative text — they are not bound to actual handlers in this code.
Browser compatibility
Uses only the contextmenu event, getBoundingClientRect, and requestAnimationFrame — available in all modern browsers without polyfill.
If JS is disabled, no custom menu appears and the browser's native menu remains active — no errors, the page stays functional.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="context-scene" id="contextScene">
<!-- Clickable area content (text, image, objects…) -->
<div class="ctx-menu" id="ctxMenu">
<button class="ctx-item">
<span class="ctx-icon">✎</span>
Edit
<span class="ctx-key">Ctrl+E</span>
</button>
<button class="ctx-item">
<span class="ctx-icon">📋</span>
Copy
<span class="ctx-key">Ctrl+C</span>
</button>
<div class="ctx-sep"></div>
<button class="ctx-item">
<span class="ctx-icon">🗑</span>
Delete
<span class="ctx-key">Del</span>
</button>
</div>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| .ctx-menu { background } | #1e1b4b | Menu background color — replace with your brand surface color (e.g. #111827 for dark gray, #1c1c1e for a macOS-style look). |
| .ctx-menu { border-color } | rgba(99,102,241,.2) | Border and hover accent color — align with your primary color. |
| .ctx-item { color } | #c7d2fe | Item text color at rest. Increase contrast to #e2e8f0 for a better WCAG ratio. |
| .ctx-menu { min-width } | 170px | Minimum menu width — increase to 200–220 px if your labels are long or icons are wider. |
| .ctx-menu { border-radius } | 12px | Menu corner radius. Use 6px for a compact style or 16px for a macOS feel. |
Edge margin (JS — value 8 in flip logic) |
8 px | Safety gap between the menu and the container edge during flip. Increase if your container has internal padding. |
FAQ
e.preventDefault() in the contextmenu handler already suppresses the native menu on the targeted area (.context-scene). If you embed this inside an iframe, also add preventDefault() in the iframe's own listener — events do not automatically cross frame boundaries.mouseenter on the parent item and position a second .ctx-menu to the right (or left if the first is near the right edge), reusing the same getBoundingClientRect calculation logic. The existing click listener on document will naturally close all levels at once.contextmenu event — so the custom menu also appears on mobile. On some browsers the native menu may flash briefly before being suppressed; adding -webkit-touch-callout: none and user-select: none to the target area reduces this behavior.