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 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
Basic Setup
<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:
<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>
Icons require the Icon system 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:
<x-wirekit::context-menu.item :danger="true" icon="trash" wire:click="delete">
Delete permanently
</x-wirekit::context-menu.item>
Disabled Items
<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:
@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
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.
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 (
contextmenuevent) opens the menu at the cursor position - Floating UI (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 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", uniqueid(auto-generated) - Items:
role="menuitem",tabindex="-1" - Disabled items:
aria-disabled="true", skipped by keyboard navigation - Focus management: the first enabled item is focused when the menu opens — right-click and long-press included, not just the keyboard path. On close, focus returns to the first focusable element inside your
triggerslot (a button or link). If the trigger area holds no focusable element there is nothing to return focus to, so give a right-clickable region one whenever the action it exposes matters - 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
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 first focusable element in the trigger slot |
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-subtle |
| 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:
'components' => [
'context-menu' => [],
],
Scoped Personalization
<x-wirekit::context-menu scope="file-manager">
...
</x-wirekit::context-menu>
'personalizations' => [
'context-menu' => [
'file-manager' => [
'base' => 'min-w-[14rem]',
],
],
],
Further Reading
- WAI-ARIA Menu Pattern -- the authoring pattern this component implements
- Floating UI -- positioning engine (bundled, ~3.5 KB)
- MDN:
role="menu" - MDN:
role="menuitem" - MDN:
contextmenuevent