---
title: Context Menu
description: Right-click context menu
visibility: guest
draft: false
---

# Context Menu

The `<x-wirekit::context-menu>` component creates a right-click triggered dropdown menu. It follows the same keyboard navigation and ARIA patterns as [Dropdown](/components/dropdown) but uses the browser's `contextmenu` event as its trigger mechanism.

On touch devices — which have no right-click — the menu also opens on a **long-press** (touch-and-hold for half a second) anywhere on the trigger area. A scroll or drag gesture cancels the press, so the menu never fights with scrolling.

## Usage

:::preview{title="Right-Click Context Menu"}
<x-wirekit::context-menu>
    <x-slot:trigger>
        <div style="padding: 2rem; border: 2px dashed var(--color-wk-border); border-radius: var(--radius-wk-md); text-align: center; color: var(--color-wk-text-muted); cursor: default;">
            Right-click this area
        </div>
    </x-slot:trigger>
    <x-wirekit::context-menu.item icon="edit">Edit</x-wirekit::context-menu.item>
    <x-wirekit::context-menu.item icon="eye">View Details</x-wirekit::context-menu.item>
    <x-wirekit::context-menu.separator />
    <x-wirekit::context-menu.item icon="download">Download</x-wirekit::context-menu.item>
    <x-wirekit::context-menu.separator />
    <x-wirekit::context-menu.item icon="trash" danger>Delete</x-wirekit::context-menu.item>
</x-wirekit::context-menu>
:::

## Basic Setup

```blade
<x-wirekit::context-menu>
    <x-slot:trigger>
        <div class="p-8 border-2 border-dashed rounded-lg">
            Right-click here
        </div>
    </x-slot:trigger>

    <x-wirekit::context-menu.item icon="edit" wire:click="edit({{ $item->id }})">
        Edit
    </x-wirekit::context-menu.item>
    <x-wirekit::context-menu.item icon="eye" href="/items/{{ $item->id }}">
        View Details
    </x-wirekit::context-menu.item>
    <x-wirekit::context-menu.separator />
    <x-wirekit::context-menu.item icon="trash" :danger="true" wire:click="delete({{ $item->id }})">
        Delete
    </x-wirekit::context-menu.item>
</x-wirekit::context-menu>
```

## Icons

Items can display icons using WireKit's icon alias system:

:::preview{title="Menu Items with Icons"}
<x-wirekit::context-menu>
    <x-slot:trigger>
        <div style="padding: 2rem; border: 2px dashed var(--color-wk-border); border-radius: var(--radius-wk-md); text-align: center; color: var(--color-wk-text-muted); cursor: default;">
            Right-click for menu with icons
        </div>
    </x-slot:trigger>
    <x-wirekit::context-menu.item icon="edit">Edit</x-wirekit::context-menu.item>
    <x-wirekit::context-menu.item icon="download">Download</x-wirekit::context-menu.item>
    <x-wirekit::context-menu.item icon="eye">Preview</x-wirekit::context-menu.item>
</x-wirekit::context-menu>
:::

```blade
<x-wirekit::context-menu.item icon="edit">Edit</x-wirekit::context-menu.item>
<x-wirekit::context-menu.item icon="download">Download</x-wirekit::context-menu.item>
<x-wirekit::context-menu.item icon="trash" :danger="true">Delete</x-wirekit::context-menu.item>
```

::: tip
Icons require the [Icon system](/components/icon) to be configured with a preset and the `blade-icons` package installed.
:::

## Danger Items

Use the `danger` prop for destructive actions. These items use the danger color tokens and signal caution to the user:

:::preview{title="Danger Items"}
<x-wirekit::context-menu>
    <x-slot:trigger>
        <div style="padding: 2rem; border: 2px dashed var(--color-wk-border); border-radius: var(--radius-wk-md); text-align: center; color: var(--color-wk-text-muted); cursor: default;">
            Right-click for destructive actions
        </div>
    </x-slot:trigger>
    <x-wirekit::context-menu.item icon="edit">Rename</x-wirekit::context-menu.item>
    <x-wirekit::context-menu.item icon="download">Duplicate</x-wirekit::context-menu.item>
    <x-wirekit::context-menu.separator />
    <x-wirekit::context-menu.item icon="trash" :danger="true">Delete permanently</x-wirekit::context-menu.item>
</x-wirekit::context-menu>
:::

```blade
<x-wirekit::context-menu.item :danger="true" icon="trash" wire:click="delete">
    Delete permanently
</x-wirekit::context-menu.item>
```

## Disabled Items

:::preview{title="Disabled Items"}
<x-wirekit::context-menu>
    <x-slot:trigger>
        <div style="padding: 2rem; border: 2px dashed var(--color-wk-border); border-radius: var(--radius-wk-md); text-align: center; color: var(--color-wk-text-muted); cursor: default;">
            Right-click for partially disabled menu
        </div>
    </x-slot:trigger>
    <x-wirekit::context-menu.item icon="edit">Edit</x-wirekit::context-menu.item>
    <x-wirekit::context-menu.item :disabled="true" icon="download">Export (no permission)</x-wirekit::context-menu.item>
    <x-wirekit::context-menu.separator />
    <x-wirekit::context-menu.item :disabled="true" icon="trash" :danger="true">Delete (locked)</x-wirekit::context-menu.item>
</x-wirekit::context-menu>
:::

```blade
<x-wirekit::context-menu.item :disabled="true" icon="download">
    Export (no permission)
</x-wirekit::context-menu.item>
```

Disabled items are visually muted, not focusable via keyboard navigation, and have `aria-disabled="true"`.

## On Table Rows

A common pattern is attaching a context menu to each record in a data list. Right-click any row to open the menu — only one context menu is ever visible at a time, so opening a new one automatically closes the previous one:

:::preview{title="Context menu on table rows"}
<div role="region" aria-label="User list — scroll horizontally on narrow viewports" tabindex="0" style="width: 100%; max-width: 42rem; overflow-x: auto;">
<div style="border: 1px solid var(--color-wk-border); border-radius: var(--radius-wk-md); overflow: hidden; font-family: var(--font-wk-sans); font-size: var(--text-wk-md); min-width: 28rem;">
    <div style="display: flex; background: var(--color-wk-bg-subtle); font-weight: 600; color: var(--color-wk-text-muted); border-bottom: 1px solid var(--color-wk-border);">
        <div style="flex: 0 0 11rem; padding: 0.625rem 0.875rem; white-space: nowrap;">Name</div>
        <div style="flex: 1 1 0; padding: 0.625rem 0.875rem;">Email</div>
        <div style="flex: 0 0 7rem; padding: 0.625rem 0.875rem;">Role</div>
    </div>
    <x-wirekit::context-menu style="display: block; border-bottom: 1px solid var(--color-wk-border-subtle);">
        <x-slot:trigger>
            <div style="display: flex;">
                <div style="flex: 0 0 11rem; padding: 0.75rem 0.875rem; white-space: nowrap;">Ada Lovelace</div>
                <div style="flex: 1 1 0; padding: 0.75rem 0.875rem; color: var(--color-wk-text-muted);">ada@example.com</div>
                <div style="flex: 0 0 7rem; padding: 0.75rem 0.875rem;">Admin</div>
            </div>
        </x-slot:trigger>
        <x-wirekit::context-menu.item icon="eye">View profile</x-wirekit::context-menu.item>
        <x-wirekit::context-menu.item icon="edit">Edit user</x-wirekit::context-menu.item>
        <x-wirekit::context-menu.separator />
        <x-wirekit::context-menu.item icon="trash" :danger="true">Delete</x-wirekit::context-menu.item>
    </x-wirekit::context-menu>
    <x-wirekit::context-menu style="display: block; border-bottom: 1px solid var(--color-wk-border-subtle);">
        <x-slot:trigger>
            <div style="display: flex;">
                <div style="flex: 0 0 11rem; padding: 0.75rem 0.875rem; white-space: nowrap;">Alan Turing</div>
                <div style="flex: 1 1 0; padding: 0.75rem 0.875rem; color: var(--color-wk-text-muted);">alan@example.com</div>
                <div style="flex: 0 0 7rem; padding: 0.75rem 0.875rem;">Editor</div>
            </div>
        </x-slot:trigger>
        <x-wirekit::context-menu.item icon="eye">View profile</x-wirekit::context-menu.item>
        <x-wirekit::context-menu.item icon="edit">Edit user</x-wirekit::context-menu.item>
        <x-wirekit::context-menu.separator />
        <x-wirekit::context-menu.item icon="trash" :danger="true">Delete</x-wirekit::context-menu.item>
    </x-wirekit::context-menu>
    <x-wirekit::context-menu style="display: block;">
        <x-slot:trigger>
            <div style="display: flex;">
                <div style="flex: 0 0 11rem; padding: 0.75rem 0.875rem; white-space: nowrap;">Grace Hopper</div>
                <div style="flex: 1 1 0; padding: 0.75rem 0.875rem; color: var(--color-wk-text-muted);">grace@example.com</div>
                <div style="flex: 0 0 7rem; padding: 0.75rem 0.875rem;">Viewer</div>
            </div>
        </x-slot:trigger>
        <x-wirekit::context-menu.item icon="eye">View profile</x-wirekit::context-menu.item>
        <x-wirekit::context-menu.item icon="edit">Edit user</x-wirekit::context-menu.item>
        <x-wirekit::context-menu.separator />
        <x-wirekit::context-menu.item icon="trash" :danger="true">Delete</x-wirekit::context-menu.item>
    </x-wirekit::context-menu>
</div>
</div>
:::

```blade
@foreach($users as $user)
    <x-wirekit::context-menu>
        <x-slot:trigger>
            <div class="grid grid-cols-3 px-3 py-2">
                <div>{{ $user->name }}</div>
                <div>{{ $user->email }}</div>
                <div>{{ $user->role }}</div>
            </div>
        </x-slot:trigger>
        <x-wirekit::context-menu.item icon="eye" href="/users/{{ $user->id }}">
            View profile
        </x-wirekit::context-menu.item>
        <x-wirekit::context-menu.item icon="edit" wire:click="edit({{ $user->id }})">
            Edit user
        </x-wirekit::context-menu.item>
        <x-wirekit::context-menu.separator />
        <x-wirekit::context-menu.item icon="trash" :danger="true" wire:click="delete({{ $user->id }})">
            Delete
        </x-wirekit::context-menu.item>
    </x-wirekit::context-menu>
@endforeach
```

::: tip
A `<div>`-based grid is used in the example above instead of a real `<table>` because `<x-wirekit::context-menu>` renders a wrapper element, and wrappers between `<tbody>` and `<tr>` are invalid HTML. If you need a true `<table>`, attach the right-click handler directly to each `<tr>` via Alpine: `<tr x-data="wirekitContextMenu()" x-on:contextmenu="openAt($event)" x-on:click.outside="close()">`.
:::

## Submenus

Nest a `<x-wirekit::context-menu.submenu>` to add a flyout sub-menu. The parent item opens a child panel beside it (on hover, click, or `ArrowRight`); the child holds ordinary `<x-wirekit::context-menu.item>`s. Selecting a leaf item closes the whole menu.

:::preview{title="Context menu with a submenu"}
<x-wirekit::context-menu>
    <x-slot:trigger>
        <x-wirekit::card>
            <x-wirekit::card.body>Right-click anywhere in this card</x-wirekit::card.body>
        </x-wirekit::card>
    </x-slot:trigger>
    <x-wirekit::context-menu.item icon="edit">Rename</x-wirekit::context-menu.item>
    <x-wirekit::context-menu.submenu label="Send to">
        <x-wirekit::context-menu.item>Desktop</x-wirekit::context-menu.item>
        <x-wirekit::context-menu.item>Documents</x-wirekit::context-menu.item>
    </x-wirekit::context-menu.submenu>
    <x-wirekit::context-menu.separator />
    <x-wirekit::context-menu.item danger icon="trash">Delete</x-wirekit::context-menu.item>
</x-wirekit::context-menu>
:::

Pass a `label` for plain parent text, or a `<x-slot:label>` for rich content. The parent carries `aria-haspopup="menu"` + `aria-expanded`; the child panel is its own `role="menu"`. Submenus nest arbitrarily deep.

## Behavior

- **Right-click** (`contextmenu` event) opens the menu at the cursor position
- **[Floating UI](https://floating-ui.com/)** (bundled ~3.5 KB) positions the menu at the click coordinates with flip and shift
- **Click outside** closes the menu automatically
- **Livewire SPA** navigation (`wire:navigate`) automatically closes open menus
- **Transitions** use scale + opacity for smooth open/close animation
- The native browser context menu is prevented only inside the trigger area -- right-clicking elsewhere on the page behaves normally

## Props

### `<x-wirekit::context-menu>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `teleport` | `bool` | `true` | Teleport the panel to `<body>` so it escapes every ancestor stacking context, `overflow: hidden`, and `contain: layout` container — the same pattern Modal, Drawer, Alert-Dialog, Tooltip, and Command-Palette use. Set to `false` only when the panel must stay inside the component root (rare; almost always an anti-pattern). |
| `scope` | `string\|null` | `null` | Scoped personalization key |

### `<x-wirekit::context-menu.item>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `href` | `string\|null` | `null` | URL (renders `<a>` instead of `<button>`) |
| `danger` | `bool` | `false` | Destructive/danger styling |
| `disabled` | `bool` | `false` | Disabled state |
| `icon` | `string\|null` | `null` | WireKit icon alias (e.g. `'trash'`, `'edit'`) |
| `scope` | `string\|null` | `null` | Scoped personalization key |

### `<x-wirekit::context-menu.submenu>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string\|null` | `null` | Parent item text (or pass a `<x-slot:label>` for rich content) |
| `icon` | `string\|null` | `null` | Optional leading icon (WireKit icon alias) |
| `placement` | `string` | `'right-start'` | [Floating UI placement](https://floating-ui.com/docs/computePosition#placement) of the child panel |
| `offset` | `int` | `0` | Distance in px between the parent item and the child panel |
| `disabled` | `bool` | `false` | Disabled state |
| `scope` | `string\|null` | `null` | Scoped personalization key |

### Slots

| Slot | Purpose |
| --- | --- |
| `trigger` | The area that responds to right-click |
| default | Menu items and separators |

## Sub-Components

| Component | Purpose |
| --- | --- |
| `context-menu.item` | A single menu entry (link or action) |
| `context-menu.submenu` | A nested flyout sub-menu opened from a parent item |
| `context-menu.separator` | Visual divider between item groups |

## Accessibility

- Menu panel: `role="menu"`, unique `id` (auto-generated)
- Items: `role="menuitem"`, `tabindex="-1"`
- Disabled items: `aria-disabled="true"`, skipped by keyboard navigation
- **Focus management:** first item is focused on open, focus returns to trigger area on close
- **Click outside** closes the menu
- **Escape** closes the menu
- Icons: `aria-hidden="true"` (decorative)
- The native browser context menu is suppressed only within the trigger area

::: warning
Context menus are a progressive enhancement. Every action available in the context menu must also be reachable through another UI path (button, dropdown menu, etc.) — keyboard and assistive-technology users never discover a right-click-only menu.
:::

## Keyboard Interaction

| Key | Action |
|-----|--------|
| `Shift+F10` (or the platform context-menu key) on the trigger | Open the context menu (keyboard equivalent of right-click) |
| `ArrowUp` / `ArrowDown` | Move between items |
| `Home` / `End` | Jump to first / last item |
| `Enter` / `Space` | Activate the focused item |
| `ArrowRight` (on a submenu parent) | Open the submenu and focus its first item |
| `ArrowLeft` / `Escape` (inside a submenu) | Close the submenu and return focus to the parent item |
| `Escape` | Close the menu and return focus to the trigger |

## Pitfalls

- **Don't use context-menu as a primary action surface.** Right-click + long-press are discoverable to <30% of users (UX research consensus). Mirror every context-menu action in a visible button or `<x-wirekit::dropdown>` so non-mouse users can reach it.
- **Don't nest context-menus.** Sub-menus are supported via nested context-menus but two-level depth is the comfortable maximum.

## Design Tokens

| Element | Token |
| --- | --- |
| Menu background | `--color-wk-bg-elevated` |
| Menu border | `--color-wk-border` / `--border-wk-width` |
| Menu radius | `--radius-wk-lg` |
| Menu shadow | `--shadow-wk-lg` |
| Menu padding | `--padding-wk-x-sm` |
| Item text | `--color-wk-text` |
| Item hover bg | `--color-wk-bg-active` |
| Item radius | `--radius-wk-md` |
| Item padding | `--padding-wk-x-md` / `--padding-wk-y-sm` |
| Danger text | `--color-wk-danger-text` |
| Danger hover bg | `--color-wk-danger` |
| Icon color | `--color-wk-text-muted` |
| Separator color | `--color-wk-border-subtle` |
| Font family | `--font-wk-sans` |
| Font size | `--text-wk-md` |
| Disabled opacity | `--opacity-wk-disabled` |

## Personalization

Override defaults in `config/wirekit.php`:

```php
'components' => [
    'context-menu' => [],
],
```

### Scoped Personalization

```blade
<x-wirekit::context-menu scope="file-manager">
    ...
</x-wirekit::context-menu>
```

```php
'personalizations' => [
    'context-menu' => [
        'file-manager' => [
            'base' => 'min-w-[14rem]',
        ],
    ],
],
```

## Further Reading

- [WAI-ARIA Menu Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu/) -- the authoring pattern this component implements
- [Floating UI](https://floating-ui.com/) -- positioning engine (bundled, ~3.5 KB)
- [MDN: `role="menu"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/menu_role)
- [MDN: `role="menuitem"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/menuitem_role)
- [MDN: `contextmenu` event](https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event)
