Default

Right click here
<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 });