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: 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
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:
'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