---
title: ContextMenu
description: Menu triggered by right-click or long press on its child content.
sidebar:
  order: 7
search:
  tags: [right click, long press, actions, menu]
---

# ContextMenu

> Menu triggered by right-click or long press on its child content.

## Import

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

## Slot Structure

Right-click trigger with a floating menu portal containing grouped items.

Use `contentProps` and `itemProps` to forward native attributes, refs, and event handlers. Item handlers run before built-in menu behavior and can call `preventDefault()` to cancel activation.

### Menu

```text
trigger
└── content (portal)
    └── group (×n)
        ├── label (optional)
        ├── separator (optional)
        └── item (×n)
```

### Item internals

```text
item
├── itemLeading (optional)
├── itemWrapper
│   ├── itemLabel (optional)
│   └── itemDescription (optional)
└── itemTrailing
    └── itemIndicator (optional, checkbox items)
```

## Examples

### Sizes

Menu item size scale from `sm` to `lg` for compact and roomy density.

```tsx
function Sizes() {
  const SIZES = ['sm', 'md', 'lg'] as const

  const ITEMS: ContextMenuT.Item[] = [
    {
      type: 'group',
      children: [
        {
          label: 'Open',
          icon: 'i-lucide-folder-open',
        },
        {
          label: 'Rename',
          icon: 'i-lucide-pencil',
        },
        {
          label: 'Delete',
          icon: 'i-lucide-trash-2',
          color: 'destructive',
        },
      ],
    },
  ]

  return (
    <div class="flex flex-wrap gap-3">
      <For each={SIZES}>
        {(size) => (
          <ContextMenu size={size} items={ITEMS}>
            <Button variant="outline">Right click ({size})</Button>
          </ContextMenu>
        )}
      </For>
    </div>
  )
}
```

### Placements

Same file menu rendered with top/right/bottom/left placements for quick transition-direction sanity checks.

```tsx
function Placements() {
  const surfaceClass =
    'flex h-24 w-full items-center justify-center rounded-lg b-1 b-border border-border bg-background text-sm text-foreground'

  const badgeClass =
    'rounded-md b-1 b-border border-border bg-muted px-1.5 py-0.5 font-medium text-[11px] text-foreground'

  const fileItems: ContextMenuT.Item[] = [
    {
      type: 'group',
      label: 'File Actions',
      children: [
        {
          label: 'Open File',
          icon: 'i-lucide-file-code-2',
          kbds: ['↵'],
        },
        {
          label: 'Open in Split View',
          icon: 'i-lucide-split-square-horizontal',
          kbds: ['⌘', '\\'],
        },
        {
          label: 'Reveal in Explorer',
          icon: 'i-lucide-folder-search-2',
        },
        { type: 'separator' },
        {
          label: 'Move To…',
          icon: 'i-lucide-folder-input',
          children: [
            {
              type: 'group',
              label: 'Recent Folders',
              children: [
                {
                  label: 'src/overlays',
                  icon: 'i-lucide-folder-open',
                },
                {
                  label: 'src/navigation',
                  icon: 'i-lucide-folder-open',
                },
                {
                  label: 'More Destinations',
                  icon: 'i-lucide-more-horizontal',
                  children: [
                    {
                      type: 'group',
                      children: [
                        {
                          label: 'docs/components',
                          icon: 'i-lucide-folder-open',
                        },
                        {
                          label: 'docs/content',
                          icon: 'i-lucide-folder-open',
                        },
                      ],
                    },
                  ],
                },
              ],
            },
          ],
        },
        {
          label: 'Copy Path',
          icon: 'i-lucide-copy',
          kbds: ['⌘', '⌥', 'C'],
        },
      ],
    },
    {
      type: 'group',
      children: [
        {
          label: (
            <div class="flex gap-2 items-center">
              <span>Rename</span>
              <span class={badgeClass}>F2</span>
            </div>
          ),
          icon: 'i-lucide-pencil',
        },
        {
          label: 'Delete',
          icon: 'i-lucide-trash-2',
          color: 'destructive',
        },
      ],
    },
  ]

  return (
    <div class="gap-3 grid sm:grid-cols-2">
      <ContextMenu placement="top" items={fileItems}>
        <div class={surfaceClass}>Right click (top)</div>
      </ContextMenu>
      <ContextMenu placement="right" items={fileItems}>
        <div class={surfaceClass}>Right click (right)</div>
      </ContextMenu>
      <ContextMenu placement="bottom" items={fileItems}>
        <div class={surfaceClass}>Right click (bottom)</div>
      </ContextMenu>
      <ContextMenu placement="left" items={fileItems}>
        <div class={surfaceClass}>Right click (left)</div>
      </ContextMenu>
    </div>
  )
}
```

### File Explorer

A file-row context menu with move flows, shortcuts, mixed labels, and destructive actions.

```tsx
function FileExplorer() {
  const badgeClass =
    'rounded-md b-1 b-border border-border bg-muted px-1.5 py-0.5 font-medium text-[11px] text-foreground'

  const [lastAction, setLastAction] = createSignal('None')

  return (
    <div class="flex flex-col gap-4">
      <ContextMenu
        items={[
          {
            type: 'group',
            label: (
              <div class="flex gap-2 items-center">
                <span>Issue: Improve menu transitions</span>
                <span class={badgeClass}>P1</span>
              </div>
            ),
            children: [
              {
                label: 'Open Issue',
                icon: 'i-lucide-external-link',
                onSelect: () => setLastAction('Open issue'),
              },
              {
                label: 'Assign',
                icon: 'i-lucide-user-round-plus',
                children: [
                  {
                    type: 'group',
                    children: [
                      {
                        label: 'Alex Morgan',
                        description: 'Design systems',
                        icon: 'i-lucide-user',
                        onSelect: () => setLastAction('Assign Alex Morgan'),
                      },
                      {
                        label: 'Jamie Chen',
                        description: 'Overlay primitives',
                        icon: 'i-lucide-user',
                        onSelect: () => setLastAction('Assign Jamie Chen'),
                      },
                    ],
                  },
                ],
              },
              {
                label: 'Move to Sprint',
                icon: 'i-lucide-calendar-range',
                children: [
                  {
                    type: 'group',
                    children: [
                      {
                        label: 'Sprint 18',
                        onSelect: () => setLastAction('Move to Sprint 18'),
                      },
                      {
                        label: 'Sprint 19',
                        onSelect: () => setLastAction('Move to Sprint 19'),
                      },
                      {
                        label: 'Backlog',
                        onSelect: () => setLastAction('Move to backlog'),
                      },
                    ],
                  },
                ],
              },
            ],
          },
          {
            type: 'group',
            children: [
              {
                label: 'Edit Details',
                icon: 'i-lucide-pencil',
                onSelect: () => setLastAction('Edit details'),
              },
              {
                label: 'Share Update',
                icon: 'i-lucide-share-2',
                onSelect: () => setLastAction('Share update'),
              },
              { type: 'separator' },
              {
                label: 'Archive',
                icon: 'i-lucide-archive',
                onSelect: () => setLastAction('Archive issue'),
              },
              {
                label: 'Delete Issue',
                icon: 'i-lucide-trash-2',
                color: 'destructive',
                onSelect: () => setLastAction('Delete issue'),
              },
            ],
          },
        ]}
      >
        <div class="text-sm text-foreground p-4 b-1 b-border border-border rounded-lg bg-background flex flex-col min-h-28 w-full justify-between">
          <div class="flex gap-3 items-center justify-between">
            <div>
              <div class="text-foreground font-medium">dropdown-menu.tsx</div>
              <div class="text-xs text-muted-foreground">src/overlays/dropdown-menu</div>
            </div>
            <span class={badgeClass}>Modified</span>
          </div>
          <div class="text-xs text-muted-foreground">Right click this file row</div>
        </div>
      </ContextMenu>
      <p class="text-sm text-muted-foreground px-4">
        Last action: <span class="font-medium">{lastAction()}</span>
      </p>
    </div>
  )
}
```

### Editor Selection

A code-editor-style context menu with refactors, toggles, and theme switching for keyboard and pointer testing.

```tsx
function EditorSelection() {
  const [showMinimap, setShowMinimap] = createSignal(true)
  const [showStickyScroll, setShowStickyScroll] = createSignal(true)
  const [showInlineHints, setShowInlineHints] = createSignal(false)
  const [editorTheme, setEditorTheme] = createSignal<'light' | 'dark' | 'system'>('dark')

  const editorItems = createMemo<ContextMenuT.Item[]>(() => [
    {
      type: 'group',
      label: 'Editor Selection',
      children: [
        {
          label: 'Quick Fix…',
          icon: 'i-lucide-wand-sparkles',
          kbds: ['⌘', '.'],
        },
        {
          label: 'Refactor',
          icon: 'i-lucide-git-branch-plus',
          children: [
            {
              type: 'group',
              children: [
                {
                  label: 'Extract Variable',
                  icon: 'i-lucide-variable',
                },
                {
                  label: 'Extract Function',
                  icon: 'i-lucide-braces',
                },
                {
                  label: 'Move to File…',
                  icon: 'i-lucide-file-output',
                },
              ],
            },
          ],
        },
        { type: 'separator' },
        {
          type: 'checkbox',
          label: 'Show Minimap',
          icon: 'i-lucide-map',
          checked: showMinimap(),
          onCheckedChange: (checked: boolean) => setShowMinimap(checked),
        },
        {
          type: 'checkbox',
          label: 'Sticky Scroll',
          icon: 'i-lucide-panel-top',
          checked: showStickyScroll(),
          onCheckedChange: (checked: boolean) => setShowStickyScroll(checked),
        },
        {
          type: 'checkbox',
          label: 'Inline Hints',
          icon: 'i-lucide-message-square-quote',
          checked: showInlineHints(),
          onCheckedChange: (checked: boolean) => setShowInlineHints(checked),
        },
      ],
    },
    {
      type: 'group',
      label: 'Theme',
      children: [
        {
          label: 'Light',
          icon: editorTheme() === 'light' ? 'i-lucide-check' : 'i-lucide-sun',
          onSelect: () => setEditorTheme('light'),
        },
        {
          label: 'Dark',
          icon: editorTheme() === 'dark' ? 'i-lucide-check' : 'i-lucide-moon',
          onSelect: () => setEditorTheme('dark'),
        },
        {
          label: 'System',
          icon: editorTheme() === 'system' ? 'i-lucide-check' : 'i-lucide-monitor',
          onSelect: () => setEditorTheme('system'),
        },
      ],
    },
  ])

  return (
    <div class="flex flex-col">
      <ContextMenu items={editorItems()}>
        <div class="text-sm text-foreground p-4 b-1 b-border border-border rounded-lg bg-background flex flex-col min-h-28 w-full justify-between">
          <div class="text-xs text-foreground font-mono">
            const motion = resolveOverlayMenuSide(placement)
          </div>
          <div class="text-xs text-muted-foreground">Right click the selected line</div>
        </div>
      </ContextMenu>
      <div class="text-sm text-muted-foreground mt-3 flex flex-wrap gap-4">
        <span>
          Minimap: <span class="font-medium">{String(showMinimap())}</span>
        </span>
        <span>
          Sticky scroll: <span class="font-medium">{String(showStickyScroll())}</span>
        </span>
        <span>
          Inline hints: <span class="font-medium">{String(showInlineHints())}</span>
        </span>
        <span>
          Theme: <span class="font-medium uppercase">{editorTheme()}</span>
        </span>
      </div>
    </div>
  )
}
```

### Project / Issue Actions

A denser project card menu with assignee and sprint submenus plus archive/delete actions.

```tsx
function ProjectIssueActions() {
  const panelClass =
    'flex min-h-28 w-full flex-col justify-between rounded-lg b-1 b-border border-border bg-background p-4 text-sm text-foreground'

  const projectItems: ContextMenuT.Item[] = [
    {
      type: 'group',
      label: (
        <div class="flex gap-2 items-center">
          <span>Issue: Improve menu transitions</span>
          <span class={badgeClass}>P1</span>
        </div>
      ),
      children: [
        {
          label: 'Open Issue',
          icon: 'i-lucide-external-link',
        },

        {
          label: 'Move to Sprint',
          icon: 'i-lucide-calendar-range',
          children: [
            {
              type: 'group',
              children: [
                {
                  label: 'Sprint 18',
                },
                {
                  label: 'Sprint 19',
                },
                {
                  label: 'Sprint 20',
                },
                {
                  label: 'Sprint 21',
                },
                {
                  label: 'Sprint 22',
                },
                {
                  label: 'Backlog',
                },
              ],
            },
          ],
        },
        {
          label: 'Edit Details',
          icon: 'i-lucide-pencil',
        },
        {
          label: 'Assign',
          icon: 'i-lucide-user-round-plus',
          children: [
            {
              type: 'group',
              children: [
                {
                  label: 'Alex Morgan',
                  description: 'Design systems',
                  icon: 'i-lucide-user',
                },
                {
                  label: 'Jamie Chen',
                  description: 'Overlay primitives',
                  icon: 'i-lucide-user',
                },
                {
                  label: 'John Smith',
                  description: 'Test',
                  icon: 'i-lucide-circle',
                },
              ],
            },
          ],
        },
        {
          label: 'Share Update',
          icon: 'i-lucide-share-2',
        },
        { type: 'separator' },
        {
          label: 'Archive',
          icon: 'i-lucide-archive',
        },
        {
          label: 'Delete Issue',
          icon: 'i-lucide-trash-2',
          color: 'destructive',
        },
      ],
    },
  ]

  return (
    <ContextMenu items={projectItems}>
      <div class={panelClass}>
        <div class="flex gap-3 items-center justify-between">
          <div>
            <div class="text-foreground font-medium">
              Improve dropdown/context menu motion polish
            </div>
            <div class="text-xs text-muted-foreground">Overlay milestone · due this sprint</div>
          </div>
          <span class={badgeClass}>In Review</span>
        </div>
        <div class="text-xs text-muted-foreground">Right click this card</div>
      </div>
    </ContextMenu>
  )
}
```

## API Reference

### Attributes

#### `trigger`

Element users activate to open the menu.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-closed | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-expanded | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| aria-controls | boolean \| string \| undefined | References the controlled element while the related content is mounted. |
| aria-expanded | boolean \| string \| undefined | Indicates whether the controlled content is expanded. |
| aria-haspopup | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |

#### `overlay`

Optional backdrop rendered behind modal menu content.

#### `content`

Positioned menu panel that contains groups, items, and submenus.

##### CSS Variables

| Attribute | Type | Description |
| --- | --- | --- |
| --mo-popper-content-transform-origin | string | CSS custom property exposed by this slot. |

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-placement | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `group`

Section wrapper for related menu items.

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `label`

Heading text for a menu group.

#### `separator`

Non-interactive divider between menu sections.

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `item`

Action row inside menu content, including checkbox and radio items.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-expanded | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-highlighted | string \| undefined | Present when the item is highlighted by pointer or keyboard navigation. |
| data-selected | string \| undefined | Present when the item is selected. |

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| aria-checked | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| aria-controls | boolean \| string \| undefined | References the controlled element while the related content is mounted. |
| aria-disabled | boolean \| string \| undefined | Indicates that the control is disabled. |
| aria-expanded | boolean \| string \| undefined | Indicates whether the controlled content is expanded. |
| aria-haspopup | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `itemLeading`

Leading icon or visual shown for a menu item.

#### `itemWrapper`

Text column that groups menu item label and description.

#### `itemLabel`

Primary text for a menu item.

#### `itemDescription`

Supporting text for a menu item.

#### `itemTrailing`

Trailing region for shortcuts, submenu arrows, or selection indicators.

#### `itemKbds`

Container for keyboard shortcut hints at the end of a menu item.

#### `itemIndicator`

Checked or selected-state indicator for checkbox and radio menu items.

#### `itemSub`

Indicator shown when a menu item opens a submenu.

### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| checkedIcon | IconT.Name | icon-check | Icon used for checked checkbox items. |
| children | JSX.Element | — | Target area that opens the context menu on right-click or long press. |
| class | ClassValue | — | Class applied to the component root or trigger element. |
| classes | (OverlayMenuSharedClasses & ContextMenuT.Classes) \| undefined | — | Slot class overrides for menu sections. |
| contentBottom | OverlayMenuContentSlot \| undefined | — | Content rendered after the resolved item groups. |
| contentProps | ElementProps<HTMLDivElement> \| undefined | — | Additional attributes for each menu layer content element. |
| 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: ContextMenuT.ItemRenderProps) => ElementProps<HTMLDivElement> \| undefined) \| undefined | — | Additional attributes for an interactive menu item. |
| itemRender | ComponentOrElement<ContextMenuT.ItemRenderProps> \| undefined | — | Custom renderer for individual items. |
| items | ContextMenuT.Item[] \| undefined | — | Items rendered in the menu body. |
| onContextMenu | JSX.EventHandlerUnion<HTMLSpanElement, MouseEvent> \| undefined | — | — |
| onKeyDown | JSX.EventHandlerUnion<HTMLSpanElement, KeyboardEvent> \| undefined | — | — |
| onOpenChange | ((open: boolean) => void) \| undefined | — | Called whenever the menu requests an open state change. |
| onPointerCancel | JSX.EventHandlerUnion<HTMLSpanElement, PointerEvent> \| undefined | — | — |
| onPointerDown | JSX.EventHandlerUnion<HTMLSpanElement, PointerEvent> \| undefined | — | — |
| onPointerMove | JSX.EventHandlerUnion<HTMLSpanElement, PointerEvent> \| undefined | — | — |
| onPointerUp | JSX.EventHandlerUnion<HTMLSpanElement, PointerEvent> \| undefined | — | — |
| 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 | OverlayMenuPlacement \| 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. |
| ref | JSX.HTMLElementTags["span"] extends { ref?: infer Ref; } ? Ref : never \| undefined | — | — |
| size | "sm" \| "md" \| "lg" \| undefined | md | Menu item size variant. |
| style | JSX.CSSProperties \| undefined | — | — |
| styles | (OverlayMenuSharedStyles & ContextMenuT.Styles) \| undefined | — | Slot style overrides for menu sections. |
| submenuIcon | IconT.Name | icon-chevron-right | Icon used for submenu trigger items. |

### Items

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| checked | boolean \| undefined | — | Controlled checked state for checkbox and radio items. |
| children | ContextMenuT.Item[] \| undefined | — | Nested menu items for creating submenus. |
| color | NonNullable<"default" \| "destructive" \| undefined> \| undefined | — | Color theme variant for the menu item. |
| defaultChecked | boolean \| undefined | — | Initial checked state for uncontrolled checkbox and radio items. |
| defaultOpen | boolean \| undefined | — | Initial open state for submenus. |
| description | JSX.Element | — | Secondary description text displayed below the label. |
| disabled | boolean \| undefined | false | Whether the item is non-interactive. |
| group | string \| undefined | — | Radio group identifier. Radio items with the same group are mutually exclusive. |
| icon | IconT.Name | — | Icon name or custom element to display at the start of the item. |
| kbds | string[] \| undefined | — | Array of keyboard shortcuts to display as keys. |
| label | JSX.Element | — | Primary label text or element. |
| onCheckedChange | ((checked: boolean) => void) \| undefined | — | Event handler called when a checkbox item's state changes. |
| onSelect | (() => void) \| undefined | — | Event handler called when the item is activated. |
| onValueChange | ((value: string) => void) \| undefined | — | Event handler called when a radio item is selected. |
| open | boolean \| undefined | — | Controlled open state for submenus. |
| type | OverlayMenuItemType \| undefined | item | The type of menu item to render. |
| value | string \| undefined | — | Radio item value reported by onValueChange and used for grouped selection. |

### ARIA

Accessibility attributes and roles emitted by the component markup.

| Attribute | Type | Description |
| --- | --- | --- |
| aria-checked | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| aria-controls | boolean \| string \| undefined | References the controlled element while the related content is mounted. |
| aria-disabled | boolean \| string \| undefined | Indicates that the control is disabled. |
| aria-expanded | boolean \| string \| undefined | Indicates whether the controlled content is expanded. |
| aria-haspopup | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| aria-hidden | boolean \| string \| undefined | Hides decorative content from assistive technology. |
| aria-label | boolean \| string \| undefined | Provides an accessible label when visible text is not sufficient. |
| role | string | Defines the semantic role exposed to assistive technology. |

### Data Attributes

State and slot attributes exposed for styling hooks and selectors.

| Attribute | Type | Description |
| --- | --- | --- |
| data-closed | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-expanded | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-highlighted | string \| undefined | Present when the item is highlighted by pointer or keyboard navigation. |
| data-placement | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-selected | string \| undefined | Present when the item is selected. |
| data-slot | string | Identifies the rendered slot for styling hooks and selectors. |
