Morphos
Overlays

Context Menu

A menu that appears at the cursor position on right-click, offering context-specific actions.

Interactive example

Open in Storybook

Installation

npm install @morphos/overlays
pnpm add @morphos/overlays
yarn add @morphos/overlays
bun add @morphos/overlays

Import

import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
} from '@morphos/overlays'

Usage

@Component()
class MyComponent extends StatefulComponent {
  @State() contextMenu = new ContextMenu()

  render() {
    return (
      <>
        <ContextMenuTrigger contextMenu={this.contextMenu}>
          Right-click anywhere in this area
        </ContextMenuTrigger>
        <ContextMenuContent contextMenu={this.contextMenu}>
          <ContextMenuItem contextMenu={this.contextMenu} label="Copy" value="copy" onSelect={() => console.log('copy')} />
          <ContextMenuItem contextMenu={this.contextMenu} label="Paste" value="paste" onSelect={() => console.log('paste')} />
          <ContextMenuItem contextMenu={this.contextMenu} label="Delete" value="delete" disabled />
        </ContextMenuContent>
      </>
    )
  }
}

Compound components

ComponentDescription
ContextMenuRoot state manager. Tracks open state and the cursor coordinates captured on contextmenu.
ContextMenuTrigger<div> that listens for the native contextmenu event, prevents the default browser menu, and opens the menu at the cursor. Sets data-open.
ContextMenuContent<ul role="menu"> rendered into a Portal, positioned with position: fixed at the captured coordinates. Closes on outside click or Escape.
ContextMenuItem<li role="menuitem">. Selectable via click or Enter/Space.

Props — ContextMenu

PropTypeDefaultDescription
onOpenChange(open: boolean) => void—Called when the menu opens or closes.
childrenChildren—Content — typically ContextMenuTrigger and ContextMenuContent.

Methods

MethodDescription
open()Opens the context menu. Emits onOpenChange(true).
close()Closes the context menu. Emits onOpenChange(false).
setPosition(x, y)Sets the cursor coordinates used to position the menu. Called internally by ContextMenuTrigger's contextmenu handler.

Unlike the other overlays in this package, ContextMenu has no open / defaultOpen props — it is always uncontrolled, since it is driven entirely by the browser's native contextmenu event rather than a click on a fixed trigger element.

Props — ContextMenuTrigger

PropTypeDefaultDescription
contextMenuContextMenu—The ContextMenu instance this trigger controls.
childrenChildren—The trigger area content.
classstring—CSS class.
idstring—HTML id.

Props — ContextMenuContent

PropTypeDefaultDescription
contextMenuContextMenu—The ContextMenu instance this content belongs to.
childrenChildren—ContextMenuItem elements.
classstring—CSS class.
idstring—HTML id.
aria-labelstring—Accessible label for the menu.

Props — ContextMenuItem

PropTypeDefaultDescription
contextMenuContextMenu—The ContextMenu instance this item belongs to.
valuestring—Unique value identifier.
labelstring—Display text, used when no children are passed.
disabledbooleanfalseDisables the item.
onSelect() => void—Called when the item is selected.
childrenChildren—Item content. Falls back to label if omitted.
classstring—CSS class.
idstring—HTML id.

Positioning

ContextMenuContent is rendered with position: fixed at the coordinates captured on the most recent contextmenu event, read from the internal _x / _y state on the ContextMenu instance. Unlike Dropdown, Popover, Tooltip, and PreviewCard, ContextMenu does not use computeAnchorPosition() from @morphos/core — there is no anchor element to measure, only a cursor position.

data-* attributes

AttributeElementWhen present
data-openContextMenuTriggerMenu is open
data-openContextMenuContent rootMenu is open
data-disabledContextMenuItemdisabled is true

ContextMenuContent renders with role="menu" and each ContextMenuItem renders with role="menuitem". The menu closes automatically when the user clicks outside, presses Escape, or selects an item.

Styling example

.morphos-context-menu-content {
  z-index: 100;
  min-width: 10rem;
  padding: var(--morphos-space-1);
  list-style: none;
  border: 1px solid var(--morphos-color-border);
  border-radius: var(--morphos-radius-md);
}

.morphos-context-menu-item[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

On this page