---
title: ContextMenu
description: Open item-specific actions at a context interaction point.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# ContextMenu

> Open item-specific actions at a context interaction point.

Use ContextMenu for actions tied to a specific surface or item at the pointer location. Keep essential actions discoverable through another visible path.

## Basic usage

```tsx
import { ContextMenu } from 'moraine'

export function Example() {
  return (
    <ContextMenu>
      <ContextMenu.Trigger>Right-click here</ContextMenu.Trigger>
      <ContextMenu.Content items={[{ label: 'Copy' }]} />
    </ContextMenu>
  )
}
```

## Anatomy

```text
ContextMenu [component; no DOM]
├── ContextMenu.Trigger [part; slot=trigger]
├── overlay [slot]
└── positioner [internal]
    └── ContextMenu.Content [part; slot=content]
        ├── group [slot]
        │   ├── groupLabel [slot]
        │   └── item [slot]
        ├── item [slot]
        │   ├── itemLeading [slot]
        │   ├── itemWrapper [slot]
        │   │   ├── itemLabel [slot]
        │   │   └── itemDescription [slot]
        │   └── itemTrailing [slot]
        │       ├── itemKbds [slot]
        │       ├── itemIndicator [slot]
        │       └── itemSubIndicator [slot]
        └── separator [slot]
```

Content is portaled; grouped items use the same item subtree as ungrouped items.

## Usage

### Triggering the menu

Wrap the target with `ContextMenu.Trigger`. The menu opens from a context-menu gesture, keyboard context-menu keys, or a supported touch/pen long press. Avoid putting essential actions only in a context menu.

```tsx
import { ContextMenu } from 'moraine'

const MENU_ITEMS = [
  { label: 'Back', leading: 'i-lucide:arrow-left' },
  { label: 'Forward', leading: 'i-lucide:arrow-right', disabled: true },
  { label: 'Reload', leading: 'i-lucide:rotate-cw' },
]

export function TriggerUsage() {
  return (
    <div class="max-w-md w-full">
      <ContextMenu>
        <ContextMenu.Trigger
          as="div"
          class="text-xs text-muted-foreground b-2 b-border rounded-xl b-dashed flex h-32 w-full select-none items-center justify-center"
        >
          Right-click or long-press inside this area
        </ContextMenu.Trigger>
        <ContextMenu.Content items={MENU_ITEMS} />
      </ContextMenu>
    </div>
  )
}
```

### Item models

Groups, separators, checkbox items, radio items, disabled items, and nested children are all represented in the public item model. Keep state for checkbox and radio values in the application and update it through item callbacks.

```tsx
import { ContextMenu } from 'moraine'

export function ItemsUsage() {
  return (
    <div class="max-w-md w-full">
      <ContextMenu>
        <ContextMenu.Trigger
          as="div"
          class="text-xs text-muted-foreground b-1 b-border rounded-xl flex h-28 w-full select-none items-center justify-center"
        >
          Right-click to view item model actions
        </ContextMenu.Trigger>
        <ContextMenu.Content
          items={[
            {
              label: 'Actions',
              children: [
                { label: 'Copy path', icon: 'i-lucide:copy' },
                { label: 'Rename file', icon: 'i-lucide:edit-2' },
                { label: 'Delete', icon: 'i-lucide:trash-2', variant: 'destructive' },
              ],
            },
          ]}
        />
      </ContextMenu>
    </div>
  )
}
```

### Keyboard interaction

| Key                                             | Description                                                |
| ----------------------------------------------- | ---------------------------------------------------------- |
| <kbd>Shift + F10</kbd> / <kbd>ContextMenu</kbd> | Opens from a focused trigger when the trigger supports it. |
| <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd>       | Moves the active item through available menu entries.      |
| <kbd>ArrowRight</kbd>                           | Opens an available submenu from its parent item.           |
| <kbd>ArrowLeft</kbd>                            | Returns from a submenu to its parent menu.                 |
| <kbd>Enter</kbd> / <kbd>Space</kbd>             | Activates the current menu item.                           |
| <kbd>Escape</kbd>                               | Closes the current menu layer.                             |

## Examples

### File actions

Move a file between folders, add it to favorites, or move it to the trash and undo. These actions update local example state; permission-dependent actions remain disabled.

```tsx
import { Button, ContextMenu, Icon } from 'moraine'
import type { ContextMenuT } from 'moraine'
import { createMemo, createSignal, Show } from 'solid-js'

export function FileActions() {
  const [folder, setFolder] = createSignal('Projects')
  const [favorite, setFavorite] = createSignal(false)
  const [deleted, setDeleted] = createSignal(false)
  const [status, setStatus] = createSignal('Right click the file, long press, or use Shift + F10.')
  const items = createMemo<ContextMenuT.Item[]>(() => [
    {
      type: 'group',
      label: 'Project brief.pdf',
      children: [
        {
          label: 'Open preview',
          icon: 'i-lucide:eye',
          onSelect: () => setStatus('Preview ready: Project brief.pdf · 4 pages · 240 KB'),
        },
        {
          type: 'checkbox',
          label: 'Add to favorites',
          checked: favorite(),
          onCheckedChange: (checked) => {
            setFavorite(checked)
            setStatus(checked ? 'Added to favorites.' : 'Removed from favorites.')
          },
        },
      ],
    },
    { type: 'separator' },
    {
      label: 'Move to',
      icon: 'i-lucide:folder-input',
      children: ['Projects', 'Shared', 'Archive'].map((destination) => ({
        label: destination,
        icon: 'i-lucide:folder',
        disabled: folder() === destination,
        onSelect: () => {
          setFolder(destination)
          setStatus(`Moved to ${destination}.`)
        },
      })),
    },
    {
      label: 'Manage access',
      description: 'Only the owner can change permissions',
      icon: 'i-lucide:lock',
      disabled: true,
    },
    { type: 'separator' },
    {
      label: 'Move to trash',
      icon: 'i-lucide:trash-2',
      variant: 'destructive',
      onSelect: () => {
        setDeleted(true)
        setStatus('Project brief.pdf moved to trash.')
      },
    },
  ])

  return (
    <section class="max-w-lg w-full space-y-3" aria-label="File actions example">
      <Show
        when={!deleted()}
        fallback={
          <div class="p-4 border border-border rounded-lg border-dashed flex gap-3 items-center justify-between">
            <span class="text-sm text-muted-foreground">File is in the trash</span>
            <Button
              variant="outline"
              onClick={() => {
                setDeleted(false)
                setStatus('Project brief.pdf restored.')
              }}
            >
              Undo
            </Button>
          </div>
        }
      >
        <ContextMenu>
          <ContextMenu.Trigger
            class="p-4 outline-none border border-border rounded-lg bg-card flex gap-3 select-none items-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            aria-label="Actions for Project brief.pdf"
          >
            <Icon name="i-lucide:file-text" class="text-muted-foreground shrink-0 size-8" />
            <div class="flex-1 min-w-0">
              <p class="text-sm font-medium truncate">Project brief.pdf</p>
              <p class="text-xs text-muted-foreground">{folder()} · 240 KB</p>
            </div>
            <Show when={favorite()}>
              <Icon name="i-lucide:star" class="text-primary size-4" />
            </Show>
          </ContextMenu.Trigger>
          <ContextMenu.Content items={items()} />
        </ContextMenu>
      </Show>
      <p role="status" class="text-xs text-muted-foreground">
        {status()}
      </p>
    </section>
  )
}
```

### Checkbox and radio items

```tsx
import { ContextMenu } from 'moraine'
import type { ContextMenuT } from 'moraine'
import { createMemo, createSignal } from 'solid-js'

export function CheckboxRadio() {
  const [pinned, setPinned] = createSignal(false)
  const [priority, setPriority] = createSignal('normal')
  const items = createMemo<ContextMenuT.Item[]>(() => [
    { type: 'checkbox', label: 'Pin item', checked: pinned(), onCheckedChange: setPinned },
    { type: 'separator' },
    {
      type: 'radio',
      label: 'Low priority',
      group: 'priority',
      value: 'low',
      checked: priority() === 'low',
      onValueChange: setPriority,
    },
    {
      type: 'radio',
      label: 'Normal priority',
      group: 'priority',
      value: 'normal',
      checked: priority() === 'normal',
      onValueChange: setPriority,
    },
    {
      type: 'radio',
      label: 'High priority',
      group: 'priority',
      value: 'high',
      checked: priority() === 'high',
      onValueChange: setPriority,
    },
  ])

  return (
    <div class="space-y-3">
      <ContextMenu>
        <ContextMenu.Trigger
          as="div"
          class="text-sm text-muted-foreground border border-border rounded-lg border-dashed flex h-28 max-w-sm select-none items-center justify-center"
        >
          Right click to change options
        </ContextMenu.Trigger>
        <ContextMenu.Content items={items()} />
      </ContextMenu>
      <p class="text-sm text-muted-foreground">
        Pinned: {String(pinned())}; priority: {priority()}
      </p>
    </div>
  )
}
```

### Submenus

```tsx
import { ContextMenu } from 'moraine'

export function Submenus() {
  return (
    <ContextMenu>
      <ContextMenu.Trigger
        as="div"
        class="text-sm text-muted-foreground border border-border rounded-lg border-dashed flex h-28 max-w-sm select-none items-center justify-center"
      >
        Right click for a submenu
      </ContextMenu.Trigger>
      <ContextMenu.Content
        items={[
          {
            label: 'Sort by',
            children: [
              {
                type: 'group',
                children: [
                  { label: 'Name', icon: 'i-lucide:arrow-down-a-z' },
                  { label: 'Modified date', icon: 'i-lucide:calendar-arrow-down' },
                ],
              },
            ],
          },
          { label: 'Refresh', icon: 'i-lucide:refresh-cw' },
        ]}
      />
    </ContextMenu>
  )
}
```

### Touch long press

```tsx
import { ContextMenu } from 'moraine'

export function LongPress() {
  return (
    <ContextMenu>
      <ContextMenu.Trigger
        as="div"
        class="text-sm text-muted-foreground text-center border border-border rounded-lg border-dashed flex h-32 max-w-sm select-none items-center justify-center touch-none"
      >
        Touch and hold for about 700 ms, or right click
      </ContextMenu.Trigger>
      <ContextMenu.Content
        items={[
          { label: 'Copy note', icon: 'i-lucide:copy' },
          { label: 'Delete note', variant: 'destructive', icon: 'i-lucide:trash-2' },
        ]}
      />
    </ContextMenu>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-closed` | `context-menu-trigger`, `context-menu-content` | Present when disclosure or transition content is closed. |
| `data-disabled` | `context-menu-trigger`, `context-menu-item` | Present when the component, slot, or item is disabled. |
| `data-expanded` | `context-menu-trigger`, `context-menu-content`, `context-menu-item` | Present when the panel, accordion, or menu is expanded. |
| `data-align` | `context-menu-content` | Stores the resolved alignment of positioned content. |
| `data-side` | `context-menu-content` | Stores the resolved floating or drawer content side. |
| `data-destructive` | `context-menu-item` | Present when the action has destructive semantics. |
| `data-highlighted` | `context-menu-item` | Present when the item is highlighted by pointer or keyboard navigation. |
| `data-selected` | `context-menu-item` | Present when the item or tab is selected. |

## Props

### ContextMenu

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| align | 'start' \| 'center' \| 'end' \| undefined | 'start' | Alignment along the cross axis. |
| defaultOpen | boolean \| undefined | false | Initial open state when the component is uncontrolled. |
| disabled | boolean \| undefined | false | Whether trigger interactions should be ignored. |
| gutter | number \| undefined | 0 | Gap between the anchor and the content. |
| id | string \| undefined | — | Unique base id used to derive trigger and content ids. |
| onOpenChange | ((open: boolean) => void) \| undefined | — | Called whenever the menu requests an open state change. |
| open | boolean \| undefined | — | Controlled open state of the menu. |
| overflowPadding | number \| undefined | 4 | Padding applied to the overflow area when calculating the menu's position. |
| placement | 'top' \| 'right' \| 'bottom' \| 'left' \| undefined | 'right' | Preferred side relative to the anchor. |
| preventScroll | boolean \| undefined | true | Whether body scroll should be locked while the menu is open. |
| shift | number \| undefined | 0 | Cross-axis or alignment offset relative to the anchor. |
| children | JSX.Element \| undefined | — | — |
| classes | Classes \| undefined | — | Family slot class defaults for this ContextMenu instance. |
| styles | Styles \| undefined | — | Family slot style defaults for this ContextMenu instance. |

### ContextMenu.Trigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| disabled | boolean \| undefined | — | Whether this trigger is disabled. |
| as | T \| undefined | 'div' | Element or component to render as. |
| children | JSX.Element \| undefined | — | Trigger label and visual content. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### ContextMenu.Content

Props for the ContextMenu component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| align | 'start' \| 'center' \| 'end' \| undefined | 'start' | Alignment along the cross axis. |
| checkedIcon | IconT.Name \| undefined | 'icon-check' | Icon used for checked checkbox items. |
| contentBottom | OverlayMenuContentSlot \| undefined | — | Content rendered after the resolved item groups. |
| contentTop | OverlayMenuContentSlot \| undefined | — | Content rendered before the resolved item groups. |
| defaultOpen | boolean \| undefined | false | Initial open state when the component is uncontrolled. |
| disabled | boolean \| undefined | false | Whether trigger interactions should be ignored. |
| gutter | number \| undefined | 0 | Gap between the anchor and the content. |
| id | string \| undefined | — | Unique base id used to derive trigger and content ids. |
| itemProps | ((props: ItemRenderProps) => ElementProps<HTMLDivElement> \| undefined) \| undefined | — | Additional attributes for an interactive menu item. |
| itemRender | ((props: ItemRenderProps) => JSX.Element) \| undefined | — | Renderer for each menu item. |
| items | (Item)[] \| undefined | — | Items rendered in the menu body. |
| onOpenChange | ((open: boolean) => void) \| undefined | — | Called whenever the menu requests an open state change. |
| open | boolean \| undefined | — | Controlled open state of the menu. |
| overflowPadding | number \| undefined | 4 | Padding applied to the overflow area when calculating the menu's position. |
| placement | 'top' \| 'right' \| 'bottom' \| 'left' \| undefined | — | Preferred content placement relative to the trigger or anchor point. |
| preventScroll | boolean \| undefined | true | Whether body scroll should be locked while the menu is open. |
| shift | number \| undefined | 0 | Cross-axis or alignment offset relative to the anchor. |
| size | 'sm' \| 'md' \| 'lg' \| undefined | 'md' | Menu item size variant. |
| submenuIcon | IconT.Name \| undefined | 'icon-chevron-right' | Icon used for submenu trigger items. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | ContentClasses \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | ContentStyles \| undefined | — | Family slot style defaults for this instance. |
