Context Menu
A menu triggered by right-click. Uses the Popover API with contextmenu event.
Default
<div class="context-menu-trigger" data-context-menu="my-ctx">
Right click here
</div>
<div class="context-menu" id="my-ctx" popover>
<button class="context-menu-item" role="menuitem">Cut</button>
<button class="context-menu-item" role="menuitem">Copy</button>
</div>
CSS view file
Styles for the context-menu component. Uses design tokens for colors, spacing, and radius.
@layer components {
.context-menu-trigger {
display: flex; align-items: center; justify-content: center;
border: 2px dashed var(--border); border-radius: var(--radius-lg);
padding: 3rem; font-size: 0.875rem; color: var(--muted-foreground); cursor: default;
}
.context-menu {
margin: 0; border: 1px solid var(--border); border-radius: var(--radius-lg);
background-color: var(--popover); color: var(--popover-foreground);
padding: 0.25rem; box-shadow: var(--shadow-md); min-width: 10rem;
opacity: 0; transform: scale(0.95);
transition: opacity 100ms ease, transform 100ms ease, display 100ms allow-discrete;
&:popover-open { opacity: 1; transform: scale(1); }
}
@starting-style {
.context-menu:popover-open { opacity: 0; transform: scale(0.95); }
}
.context-menu-item {
display: flex; align-items: center; gap: 0.5rem; width: 100%;
padding: 0.375rem 0.5rem; border: none; border-radius: var(--radius-md);
background: transparent; color: var(--popover-foreground);
font-size: 0.8125rem; font-family: inherit; text-align: left; cursor: pointer;
transition: background-color 100ms ease;
&:hover, &[data-highlighted] { background-color: var(--accent); color: var(--accent-foreground); }
&:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
}
.context-menu-separator { height: 1px; background-color: var(--border); margin: 0.25rem -0.25rem; }
}
JavaScript view file
Interaction logic for the context-menu component. Uses data attributes for wiring.
// -- Context Menu ---------------------------------------------
// Right-click context menu using the Popover API.
function init() {
document.querySelectorAll('[data-context-menu]:not([data-init])').forEach((trigger) => {
trigger.dataset.init = '';
const menu = document.getElementById(trigger.dataset.contextMenu);
if (!menu) return;
trigger.addEventListener('contextmenu', (e) => {
e.preventDefault();
menu.style.position = 'fixed';
menu.style.top = `${e.clientY}px`;
menu.style.left = `${e.clientX}px`;
menu.showPopover();
});
menu.addEventListener('click', (e) => {
if (e.target.closest('.context-menu-item')) menu.hidePopover();
});
});
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });