- Installation
- Usage
- Examples
- Basic
- Submenu
- Shortcuts
- Groups
- Icons
- Checkboxes
- Radio
- Destructive
- Sides
- RTL
- Accessibility
- API Reference
-
ContextMenu -
ContextMenu:CheckboxItem -
ContextMenu:Content -
ContextMenu:Group -
ContextMenu:Item -
ContextMenu:Label -
ContextMenu:RadioGroup -
ContextMenu:RadioItem -
ContextMenu:Shortcut -
ContextMenu:Sub -
ContextMenu:SubContent -
ContextMenu:SubTrigger -
ContextMenu:Trigger
View as Markdown
Context Menu
Displays a menu of actions triggered by a right click.
100%
Loading...
<div class="w-full max-w-xs" style="min-height: 330px">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content class="w-48">
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>
Back
<twig:ContextMenu:Shortcut>⌘[</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item disabled>
Forward
<twig:ContextMenu:Shortcut>⌘]</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
Reload
<twig:ContextMenu:Shortcut>⌘R</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Sub>
<twig:ContextMenu:SubTrigger>More Tools</twig:ContextMenu:SubTrigger>
<twig:ContextMenu:SubContent class="w-44">
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>Save Page...</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Create Shortcut...</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Name Window...</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>Developer Tools</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item variant="destructive">Delete</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:SubContent>
</twig:ContextMenu:Sub>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:CheckboxItem checked>Show Bookmarks</twig:ContextMenu:CheckboxItem>
<twig:ContextMenu:CheckboxItem>Show Full URLs</twig:ContextMenu:CheckboxItem>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:RadioGroup value="pedro">
<twig:ContextMenu:Label>People</twig:ContextMenu:Label>
<twig:ContextMenu:RadioItem value="pedro" checked>Pedro Duarte</twig:ContextMenu:RadioItem>
<twig:ContextMenu:RadioItem value="colm">Colm Tuite</twig:ContextMenu:RadioItem>
</twig:ContextMenu:RadioGroup>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
Installation
Note
Available since UX Toolkit 3.6.
php bin/console ux:install context-menu --kit shadcn
Install the following Composer dependencies:
composer require twig/extra-bundle twig/html-extra:^3.24.0 symfony/ux-icons symfony/ux-twig-component:^3.5 tales-from-a-dev/twig-tailwind-extra:^1.3.0
Install the following front-end dependencies:
npm install shadcn tw-animate-css
# ...or with Symfony AssetMapper
php bin/console importmap:require tw-animate-css/dist/tw-animate.css shadcn/dist/tailwind.css
Copy the following file(s) into your app:
assets/controllers/context_menu_controller.js
import { Controller } from '@hotwired/stimulus';
const LONG_PRESS_DELAY = 700;
const LONG_PRESS_TOLERANCE = 10;
const VIEWPORT_MARGIN = 8;
export default class extends Controller {
static targets = ['trigger', 'content', 'item'];
connect() {
this._onOutsidePointerDown = this._onOutsidePointerDown.bind(this);
this._onViewportChange = this._onViewportChange.bind(this);
this._longPress = null;
this._setState(false);
}
disconnect() {
this.cancelLongPress();
this._removeListeners();
}
open(event) {
event.preventDefault();
this.cancelLongPress();
const hasPointer = event.clientX !== 0 || event.clientY !== 0;
const anchor = hasPointer ? { x: event.clientX, y: event.clientY } : this._triggerOrigin();
this._openAt(anchor);
}
close() {
this._setState(false);
this._removeListeners();
this.dispatch('close');
}
startLongPress(event) {
if (event.pointerType === 'mouse') {
return;
}
this.cancelLongPress();
const anchor = { x: event.clientX, y: event.clientY };
this._longPress = {
anchor,
timeout: setTimeout(() => {
this._longPress = null;
this._openAt(anchor);
}, LONG_PRESS_DELAY),
};
}
moveLongPress(event) {
if (!this._longPress) {
return;
}
const { anchor } = this._longPress;
if (Math.hypot(event.clientX - anchor.x, event.clientY - anchor.y) > LONG_PRESS_TOLERANCE) {
this.cancelLongPress();
}
}
cancelLongPress() {
if (this._longPress) {
clearTimeout(this._longPress.timeout);
this._longPress = null;
}
}
preventContextMenu(event) {
event.preventDefault();
}
closeFromItem(event) {
const item = event.currentTarget;
if (this._isItemDisabled(item)) {
event.preventDefault();
return;
}
if (item.dataset.closeOnSelect !== 'false') {
this.close();
}
}
toggleCheckbox(event) {
const item = event.currentTarget;
if (this._isItemDisabled(item)) {
event.preventDefault();
return;
}
const checked = item.getAttribute('aria-checked') !== 'true';
item.setAttribute('aria-checked', String(checked));
const indicator = item.querySelector('[data-checkbox-indicator]');
if (indicator) {
indicator.hidden = !checked;
}
if (item.dataset.closeOnSelect === 'true') {
this.close();
}
}
selectRadio(event) {
const item = event.currentTarget;
if (this._isItemDisabled(item)) {
event.preventDefault();
return;
}
const group = item.closest('[data-slot="context-menu-radio-group"]');
if (!group) {
return;
}
group.dataset.value = item.dataset.value;
for (const radio of group.querySelectorAll('[role="menuitemradio"]')) {
const selected = radio.dataset.value === item.dataset.value;
radio.setAttribute('aria-checked', String(selected));
const indicator = radio.querySelector('[data-radio-indicator]');
if (indicator) {
indicator.hidden = !selected;
}
}
if (item.dataset.closeOnSelect === 'true') {
this.close();
}
}
onContentKeydown(event) {
switch (event.key) {
case 'Escape':
event.preventDefault();
this.close();
break;
case 'ArrowDown':
event.preventDefault();
this._focusNextItem();
break;
case 'ArrowUp':
event.preventDefault();
this._focusPrevItem();
break;
case 'Home':
event.preventDefault();
this._focusFirstItem();
break;
case 'End':
event.preventDefault();
this._focusLastItem();
break;
case 'Tab':
event.preventDefault();
this.close();
break;
}
}
_openAt(anchor) {
this._setState(true);
this._position(anchor);
document.addEventListener('pointerdown', this._onOutsidePointerDown, true);
window.addEventListener('resize', this._onViewportChange);
window.addEventListener('scroll', this._onViewportChange, true);
this.contentTarget.focus({ preventScroll: true });
}
_removeListeners() {
document.removeEventListener('pointerdown', this._onOutsidePointerDown, true);
window.removeEventListener('resize', this._onViewportChange);
window.removeEventListener('scroll', this._onViewportChange, true);
}
_triggerOrigin() {
const rect = this.triggerTarget.getBoundingClientRect();
return { x: rect.left, y: rect.top };
}
_position({ x, y }) {
const content = this.contentTarget;
const side = content.dataset.side;
const width = content.offsetWidth;
const height = content.offsetHeight;
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
let left = side === 'left' ? x - width : x;
let top = side === 'top' ? y - height : y;
let originX = side === 'left' ? 'right' : 'left';
let originY = side === 'top' ? 'bottom' : 'top';
if (side === 'left' ? left < VIEWPORT_MARGIN : left + width > viewportWidth - VIEWPORT_MARGIN) {
left = side === 'left' ? x : x - width;
originX = side === 'left' ? 'left' : 'right';
}
if (side === 'top' ? top < VIEWPORT_MARGIN : top + height > viewportHeight - VIEWPORT_MARGIN) {
top = side === 'top' ? y : y - height;
originY = side === 'top' ? 'top' : 'bottom';
}
left = Math.max(VIEWPORT_MARGIN, Math.min(left, viewportWidth - width - VIEWPORT_MARGIN));
top = Math.max(VIEWPORT_MARGIN, Math.min(top, viewportHeight - height - VIEWPORT_MARGIN));
content.style.left = `${left}px`;
content.style.top = `${top}px`;
content.style.transformOrigin = `${originX} ${originY}`;
}
_setState(isOpen) {
const state = isOpen ? 'open' : 'closed';
this.element.dataset.state = state;
if (this.hasTriggerTarget) {
this.triggerTarget.dataset.state = state;
}
if (this.hasContentTarget) {
this.contentTarget.dataset.state = state;
}
}
_onOutsidePointerDown(event) {
if (!this.contentTarget.contains(event.target)) {
this.close();
}
}
_onViewportChange(event) {
if (event.type === 'scroll' && this.contentTarget.contains(event.target)) {
return;
}
this.close();
}
_isItemDisabled(item) {
return (
!item ||
item.getAttribute('aria-disabled') === 'true' ||
item.dataset.disabled === 'true' ||
item.hasAttribute('disabled')
);
}
_enabledItems() {
// Exclude items nested in a sub-menu: they are not part of this menu's roving focus.
return this.itemTargets.filter(
(item) => !this._isItemDisabled(item) && !item.closest('[data-slot="context-menu-sub-content"]')
);
}
_focusFirstItem() {
this._enabledItems()[0]?.focus();
}
_focusLastItem() {
const items = this._enabledItems();
items[items.length - 1]?.focus();
}
_focusNextItem() {
const items = this._enabledItems();
if (items.length === 0) return;
const index = items.indexOf(document.activeElement);
items[index === -1 ? 0 : (index + 1) % items.length].focus();
}
_focusPrevItem() {
const items = this._enabledItems();
if (items.length === 0) return;
const index = items.indexOf(document.activeElement);
items[index === -1 ? items.length - 1 : (index - 1 + items.length) % items.length].focus();
}
}
assets/controllers/context_menu_sub_controller.js
import { Controller } from '@hotwired/stimulus';
const VIEWPORT_MARGIN = 8;
export default class extends Controller {
static targets = ['trigger', 'content'];
static values = {
openDelay: { type: Number, default: 0 },
closeDelay: { type: Number, default: 0 },
};
connect() {
this.openTimeout = null;
this.closeTimeout = null;
this.#setState('closed');
}
disconnect() {
this.#clearTimeouts();
}
show() {
this.#clearTimeouts();
this.openTimeout = setTimeout(() => {
this.#open();
this.openTimeout = null;
}, this.openDelayValue);
}
hide() {
this.#clearTimeouts();
this.closeTimeout = setTimeout(() => {
this.#setState('closed');
this.closeTimeout = null;
}, this.closeDelayValue);
}
close(event) {
if (!event.target.contains(this.element)) {
return;
}
this.#clearTimeouts();
this.#setState('closed');
}
focusOut(event) {
if (!this.element.contains(event.relatedTarget)) {
this.hide();
}
}
onKeydown(event) {
if (!this.hasTriggerTarget || !this.hasContentTarget) {
return;
}
const rtl = getComputedStyle(this.element).direction === 'rtl';
const enterKey = rtl ? 'ArrowLeft' : 'ArrowRight';
const exitKey = rtl ? 'ArrowRight' : 'ArrowLeft';
if (event.target === this.triggerTarget) {
if ([enterKey, 'Enter', ' '].includes(event.key)) {
event.preventDefault();
event.stopPropagation();
this.#clearTimeouts();
this.#open();
this.#items()[0]?.focus();
}
return;
}
if (event.target.closest('[data-slot="context-menu-sub-content"]') !== this.contentTarget) {
return;
}
const items = this.#items();
const index = items.indexOf(event.target);
let next;
switch (event.key) {
case exitKey:
next = this.triggerTarget;
break;
case 'ArrowDown':
next = items[index === -1 ? 0 : (index + 1) % items.length];
break;
case 'ArrowUp':
next = items[index === -1 ? items.length - 1 : (index - 1 + items.length) % items.length];
break;
case 'Home':
next = items[0];
break;
case 'End':
next = items[items.length - 1];
break;
default:
return;
}
event.preventDefault();
event.stopPropagation();
next?.focus();
}
#open() {
this.#place();
this.#setState('open');
}
#items() {
return [...this.contentTarget.querySelectorAll('[data-context-menu-target="item"]')].filter(
(item) =>
item.closest('[data-slot="context-menu-sub-content"]') === this.contentTarget &&
item.getAttribute('aria-disabled') !== 'true'
);
}
// State lives on the trigger and content directly (not a parent group) so nested
// sub-menus each react only to their own state.
#setState(state) {
this.element.dataset.state = state;
if (this.hasTriggerTarget) {
this.triggerTarget.dataset.state = state;
}
if (this.hasContentTarget) {
this.contentTarget.dataset.state = state;
}
}
#place() {
if (!this.hasContentTarget) {
return;
}
const content = this.contentTarget;
const anchor = this.element.getBoundingClientRect();
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
const rtl = getComputedStyle(this.element).direction === 'rtl';
const needed = content.offsetWidth + parseFloat(getComputedStyle(content).marginInlineStart);
const endSpace = rtl ? anchor.left - VIEWPORT_MARGIN : viewportWidth - VIEWPORT_MARGIN - anchor.right;
const startSpace = rtl ? viewportWidth - VIEWPORT_MARGIN - anchor.right : anchor.left - VIEWPORT_MARGIN;
const side = needed <= endSpace || endSpace >= startSpace ? 'inline-end' : 'inline-start';
if (content.dataset.side !== side) {
content.style.transition = 'none';
content.dataset.side = side;
// Commits the new side's closed offset, so the open transition slides from that side.
content.getBoundingClientRect();
content.style.transition = '';
}
const overflow = anchor.top + content.offsetHeight - (viewportHeight - VIEWPORT_MARGIN);
const shift = Math.min(Math.max(overflow, 0), Math.max(anchor.top - VIEWPORT_MARGIN, 0));
content.style.top = shift > 0 ? `${-shift}px` : '';
}
#clearTimeouts() {
if (this.openTimeout) {
clearTimeout(this.openTimeout);
this.openTimeout = null;
}
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
this.closeTimeout = null;
}
}
}
templates/components/ContextMenu.html.twig
<div
data-slot="context-menu"
{{ attributes.defaults({
class: 'contents'|tailwind_classes,
'data-controller': 'context-menu',
}) }}
>
{##- A `ContextMenu:Trigger` and a `ContextMenu:Content`. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/ContextMenu/CheckboxItem.html.twig
{%- props
## boolean Whether the checkbox is initially checked.
checked = false,
## boolean Whether the item is disabled.
disabled = false,
## boolean Whether selecting the item closes the menu.
closeOnSelect = false
-%}
<button
type="button"
role="menuitemcheckbox"
tabindex="-1"
data-slot="context-menu-checkbox-item"
data-context-menu-target="item"
data-close-on-select="{{ closeOnSelect ? 'true' : 'false' }}"
data-disabled="{{ disabled ? 'true' : 'false' }}"
aria-checked="{{ checked ? 'true' : 'false' }}"
aria-disabled="{{ disabled ? 'true' : 'false' }}"
{%- if disabled %} disabled{% endif %}
{{ attributes.defaults({
class: 'relative flex w-full cursor-default select-none items-center gap-1.5 rounded-md py-1 ps-1.5 pe-8 text-sm outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*=size-])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0'|tailwind_classes,
'data-action': 'click->context-menu#toggleCheckbox',
}) }}
>
<span class="pointer-events-none absolute end-2 flex items-center justify-center" data-checkbox-indicator {% if not checked %}hidden{% endif %}>
<twig:ux:icon name="lucide:check" class="size-4" aria-hidden="true" />
</span>
{##- The label text. -#}
{%- block content %}{% endblock -%}
</button>
templates/components/ContextMenu/Content.html.twig
{%- props
## 'top'|'right'|'bottom'|'left' Which side of the pointer the menu appears on.
side = 'bottom'
-%}
<div
role="menu"
tabindex="-1"
aria-orientation="vertical"
data-slot="context-menu-content"
data-context-menu-target="content"
data-state="closed"
data-side="{{ side }}"
{{ attributes.defaults({
class: 'invisible fixed left-0 top-0 z-50 min-w-36 scale-95 rounded-lg bg-popover p-1 text-popover-foreground opacity-0 shadow-md ring-1 ring-foreground/10 outline-hidden transition-[opacity,scale,visibility] duration-100 ease-out data-[state=open]:transition-[opacity,scale] data-[state=open]:visible data-[state=open]:scale-100 data-[state=open]:opacity-100'|tailwind_classes,
'data-action': 'keydown->context-menu#onContentKeydown contextmenu->context-menu#preventContextMenu',
}) }}
>
{##- The menu items, e.g. `ContextMenu:Item`, `ContextMenu:Separator`. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/ContextMenu/Group.html.twig
<div role="group" data-slot="context-menu-group" {{ attributes }}>
{##- One or more `ContextMenu:Item` components. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/ContextMenu/Item.html.twig
{%- props
## 'default'|'destructive' The visual style variant.
variant = 'default',
## boolean Whether the item is disabled.
disabled = false,
## boolean Whether the item is inset with extra start padding.
inset = false,
## boolean Whether selecting the item closes the menu.
closeOnSelect = true
-%}
{%- set style = html_cva(
base: 'group/context-menu-item relative flex w-full cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*=size-])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0',
variants: {
variant: {
default: 'hover:**:text-accent-foreground focus:**:text-accent-foreground',
destructive: 'text-destructive hover:bg-destructive/10 hover:text-destructive focus:bg-destructive/10 focus:text-destructive dark:hover:bg-destructive/20 dark:focus:bg-destructive/20 *:[svg]:text-destructive',
},
inset: {
true: 'ps-7',
},
},
) -%}
<button
type="button"
role="menuitem"
tabindex="-1"
data-slot="context-menu-item"
data-context-menu-target="item"
data-variant="{{ variant }}"
data-close-on-select="{{ closeOnSelect ? 'true' : 'false' }}"
data-disabled="{{ disabled ? 'true' : 'false' }}"
aria-disabled="{{ disabled ? 'true' : 'false' }}"
{%- if disabled %} disabled{% endif %}
{{ attributes.defaults({
class: style.apply({variant: variant, inset: inset ? 'true' : 'false'})|tailwind_classes,
'data-action': 'click->context-menu#closeFromItem',
}) }}
>
{##- The item label. -#}
{%- block content %}{% endblock -%}
</button>
templates/components/ContextMenu/Label.html.twig
{%- props
## boolean Whether the label is inset with extra start padding.
inset = false
-%}
{%- set style = html_cva(
base: 'px-1.5 py-1 text-xs font-medium text-muted-foreground',
variants: {
inset: {
true: 'ps-7',
},
},
) -%}
<div
data-slot="context-menu-label"
{{ attributes.defaults({
class: style.apply({inset: inset ? 'true' : 'false'})|tailwind_classes,
}) }}
>
{##- The label text. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/ContextMenu/RadioGroup.html.twig
{%- props
## string|null The currently selected value.
value = null
-%}
<div
role="group"
data-slot="context-menu-radio-group"
data-value="{{ value }}"
{{ attributes }}
>
{##- One or more `ContextMenu:RadioItem` components. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/ContextMenu/RadioItem.html.twig
{%- props
## string The value submitted when this radio is selected.
value,
## boolean Whether this radio is initially checked.
checked = false,
## boolean Whether the item is disabled.
disabled = false,
## boolean Whether selecting the item closes the menu.
closeOnSelect = false
-%}
<button
type="button"
role="menuitemradio"
tabindex="-1"
data-slot="context-menu-radio-item"
data-context-menu-target="item"
data-value="{{ value }}"
data-close-on-select="{{ closeOnSelect ? 'true' : 'false' }}"
data-disabled="{{ disabled ? 'true' : 'false' }}"
aria-checked="{{ checked ? 'true' : 'false' }}"
aria-disabled="{{ disabled ? 'true' : 'false' }}"
{%- if disabled %} disabled{% endif %}
{{ attributes.defaults({
class: 'relative flex w-full cursor-default select-none items-center gap-1.5 rounded-md py-1 ps-1.5 pe-8 text-sm outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*=size-])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0'|tailwind_classes,
'data-action': 'click->context-menu#selectRadio',
}) }}
>
<span class="pointer-events-none absolute end-2 flex items-center justify-center" data-radio-indicator {% if not checked %}hidden{% endif %}>
<twig:ux:icon name="lucide:check" class="size-4" aria-hidden="true" />
</span>
{##- The label text. -#}
{%- block content %}{% endblock -%}
</button>
templates/components/ContextMenu/Separator.html.twig
<div
role="separator"
aria-orientation="horizontal"
data-slot="context-menu-separator"
{{ attributes.defaults({
class: '-mx-1 my-1 h-px bg-border'|tailwind_classes,
}) }}
></div>
templates/components/ContextMenu/Shortcut.html.twig
<span
data-slot="context-menu-shortcut"
{{ attributes.defaults({
class: 'ms-auto text-xs tracking-widest text-muted-foreground group-focus/context-menu-item:text-accent-foreground'|tailwind_classes,
}) }}
>
{##- The shortcut label, e.g. "⌘K". -#}
{%- block content %}{% endblock -%}
</span>
templates/components/ContextMenu/Sub.html.twig
{%- props
## number Delay in milliseconds before showing the sub-menu.
openDelay = 0,
## number Delay in milliseconds before hiding the sub-menu.
closeDelay = 0
-%}
<div
data-slot="context-menu-sub"
data-context-menu-sub-open-delay-value="{{ openDelay }}"
data-context-menu-sub-close-delay-value="{{ closeDelay }}"
{{ attributes.defaults({
class: 'relative'|tailwind_classes,
'data-controller': 'context-menu-sub',
'data-action': 'mouseenter->context-menu-sub#show mouseleave->context-menu-sub#hide focusout->context-menu-sub#focusOut keydown->context-menu-sub#onKeydown context-menu:close@window->context-menu-sub#close',
}) }}
>
{##- A `ContextMenu:SubTrigger` and a `ContextMenu:SubContent`. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/ContextMenu/SubContent.html.twig
<div
role="menu"
data-slot="context-menu-sub-content"
data-context-menu-sub-target="content"
data-state="closed"
data-side="inline-end"
{{ attributes.defaults({
class: 'invisible opacity-0 transition-[opacity,translate,visibility] duration-100 ease-out data-[state=open]:transition-[opacity,translate] data-[state=open]:visible data-[state=open]:opacity-100 data-[state=open]:translate-x-0 data-[state=closed]:ltr:-translate-x-2 data-[state=closed]:rtl:translate-x-2 data-[side=inline-start]:data-[state=closed]:ltr:translate-x-2 data-[side=inline-start]:data-[state=closed]:rtl:-translate-x-2 absolute data-[side=inline-end]:start-full data-[side=inline-start]:end-full top-0 z-50 mx-1 w-max min-w-32 rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg outline-hidden'|tailwind_classes,
}) }}
>
{##- The sub-menu items, e.g. `ContextMenu:Item`. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/ContextMenu/SubTrigger.html.twig
{%- props
## boolean Whether the trigger is inset with extra start padding.
inset = false
-%}
{%- set style = html_cva(
base: 'relative flex w-full cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*=size-])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0',
variants: {
inset: {
true: 'ps-7',
},
},
) -%}
<button
type="button"
role="menuitem"
tabindex="-1"
aria-haspopup="menu"
data-slot="context-menu-sub-trigger"
data-context-menu-target="item"
data-context-menu-sub-target="trigger"
data-state="closed"
{{ attributes.defaults({
class: style.apply({inset: inset ? 'true' : 'false'})|tailwind_classes,
'data-action': 'mouseenter->context-menu-sub#show focus->context-menu-sub#show',
}) }}
>
{##- The sub-menu trigger label. -#}
{%- block content %}{% endblock -%}
<twig:ux:icon name="lucide:chevron-right" class="ms-auto size-4 rtl:rotate-180" aria-hidden="true" />
</button>
templates/components/ContextMenu/Trigger.html.twig
<div
data-slot="context-menu-trigger"
data-context-menu-target="trigger"
{{ attributes.defaults({
class: 'select-none [-webkit-touch-callout:none]'|tailwind_classes,
'data-action': 'contextmenu->context-menu#open pointerdown->context-menu#startLongPress pointermove->context-menu#moveLongPress pointerup->context-menu#cancelLongPress pointercancel->context-menu#cancelLongPress',
}) }}
>
{##- The region that opens the menu on right click or long press. -#}
{%- block content %}{% endblock -%}
</div>
Usage
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex h-40 items-center justify-center rounded-xl border border-dashed">
Right click here
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content side="bottom">
<twig:ContextMenu:Item>Back</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Reload</twig:ContextMenu:Item>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Item variant="destructive">Delete</twig:ContextMenu:Item>
</twig:ContextMenu:Content>
</twig:ContextMenu>
Examples
Basic
100%
Loading...
<div class="w-full max-w-xs">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content>
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>Back</twig:ContextMenu:Item>
<twig:ContextMenu:Item disabled>Forward</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Reload</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
Submenu
Use ContextMenu:Sub to nest secondary actions.
100%
Loading...
<div class="w-full max-w-xs" style="min-height: 280px">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content>
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>
Copy
<twig:ContextMenu:Shortcut>⌘C</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
Cut
<twig:ContextMenu:Shortcut>⌘X</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Sub>
<twig:ContextMenu:SubTrigger>More Tools</twig:ContextMenu:SubTrigger>
<twig:ContextMenu:SubContent>
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>Save Page...</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Create Shortcut...</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Name Window...</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>Developer Tools</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item variant="destructive">Delete</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:SubContent>
</twig:ContextMenu:Sub>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
Shortcuts
Add ContextMenu:Shortcut to show keyboard hints.
100%
Loading...
<div class="w-full max-w-xs" style="min-height: 210px">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content>
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>
Back
<twig:ContextMenu:Shortcut>⌘[</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item disabled>
Forward
<twig:ContextMenu:Shortcut>⌘]</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
Reload
<twig:ContextMenu:Shortcut>⌘R</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>
Save
<twig:ContextMenu:Shortcut>⌘S</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
Save As...
<twig:ContextMenu:Shortcut>⇧⌘S</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
Groups
100%
Loading...
<div class="w-full max-w-xs" style="min-height: 390px">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content>
<twig:ContextMenu:Group>
<twig:ContextMenu:Label>File</twig:ContextMenu:Label>
<twig:ContextMenu:Item>
New File
<twig:ContextMenu:Shortcut>⌘N</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
Open File
<twig:ContextMenu:Shortcut>⌘O</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
Save
<twig:ContextMenu:Shortcut>⌘S</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Label>Edit</twig:ContextMenu:Label>
<twig:ContextMenu:Item>
Undo
<twig:ContextMenu:Shortcut>⌘Z</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
Redo
<twig:ContextMenu:Shortcut>⇧⌘Z</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>
Cut
<twig:ContextMenu:Shortcut>⌘X</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
Copy
<twig:ContextMenu:Shortcut>⌘C</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
Paste
<twig:ContextMenu:Shortcut>⌘V</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item variant="destructive">
Delete
<twig:ContextMenu:Shortcut>⌫</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
Icons
100%
Loading...
<div class="w-full max-w-xs">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content>
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>
<twig:ux:icon name="lucide:copy" />
Copy
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
<twig:ux:icon name="lucide:scissors" />
Cut
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
<twig:ux:icon name="lucide:clipboard-paste" />
Paste
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item variant="destructive">
<twig:ux:icon name="lucide:trash" />
Delete
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
Checkboxes
Use ContextMenu:CheckboxItem for toggles.
100%
Loading...
<div class="w-full max-w-xs">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content>
<twig:ContextMenu:Group>
<twig:ContextMenu:CheckboxItem checked>Show Bookmarks Bar</twig:ContextMenu:CheckboxItem>
<twig:ContextMenu:CheckboxItem>Show Full URLs</twig:ContextMenu:CheckboxItem>
<twig:ContextMenu:CheckboxItem checked>Show Developer Tools</twig:ContextMenu:CheckboxItem>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
Radio
Use ContextMenu:RadioItem for exclusive choices.
100%
Loading...
<div class="w-full max-w-xs" style="min-height: 260px">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content>
<twig:ContextMenu:Group>
<twig:ContextMenu:Label>People</twig:ContextMenu:Label>
<twig:ContextMenu:RadioGroup value="pedro">
<twig:ContextMenu:RadioItem value="pedro" checked>Pedro Duarte</twig:ContextMenu:RadioItem>
<twig:ContextMenu:RadioItem value="colm">Colm Tuite</twig:ContextMenu:RadioItem>
</twig:ContextMenu:RadioGroup>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Label>Theme</twig:ContextMenu:Label>
<twig:ContextMenu:RadioGroup value="light">
<twig:ContextMenu:RadioItem value="light" checked>Light</twig:ContextMenu:RadioItem>
<twig:ContextMenu:RadioItem value="dark">Dark</twig:ContextMenu:RadioItem>
<twig:ContextMenu:RadioItem value="system">System</twig:ContextMenu:RadioItem>
</twig:ContextMenu:RadioGroup>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
Destructive
Use variant="destructive" to style the menu item as destructive.
100%
Loading...
<div class="w-full max-w-xs">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content>
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>
<twig:ux:icon name="lucide:pencil" />
Edit
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
<twig:ux:icon name="lucide:share" />
Share
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item variant="destructive">
<twig:ux:icon name="lucide:trash" />
Delete
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
Sides
Use the side prop on ContextMenu:Content to choose which side of the pointer the menu appears on.
100%
Loading...
<div class="grid w-full max-w-sm grid-cols-2 gap-4">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click (top)</span>
<span class="hidden pointer-coarse:inline-block">Long press (top)</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content side="top">
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>Back</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Forward</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Reload</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click (right)</span>
<span class="hidden pointer-coarse:inline-block">Long press (right)</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content side="right">
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>Back</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Forward</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Reload</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click (bottom)</span>
<span class="hidden pointer-coarse:inline-block">Long press (bottom)</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content side="bottom">
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>Back</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Forward</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Reload</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">Right click (left)</span>
<span class="hidden pointer-coarse:inline-block">Long press (left)</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content side="left">
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>Back</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Forward</twig:ContextMenu:Item>
<twig:ContextMenu:Item>Reload</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
RTL
To enable RTL support, set the dir="rtl" attribute on the root element.
100%
Loading...
<div class="flex w-full flex-col gap-6">
<div dir="rtl">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">انقر بزر الماوس الأيمن هنا</span>
<span class="hidden pointer-coarse:inline-block">اضغط مطولاً هنا</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content class="w-48">
<twig:ContextMenu:Group>
<twig:ContextMenu:Sub>
<twig:ContextMenu:SubTrigger>التنقل</twig:ContextMenu:SubTrigger>
<twig:ContextMenu:SubContent class="w-44">
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>
<twig:ux:icon name="lucide:arrow-left" />
رجوع
<twig:ContextMenu:Shortcut>⌘[</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item disabled>
<twig:ux:icon name="lucide:arrow-right" />
تقدم
<twig:ContextMenu:Shortcut>⌘]</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
<twig:ux:icon name="lucide:rotate-cw" />
إعادة تحميل
<twig:ContextMenu:Shortcut>⌘R</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:SubContent>
</twig:ContextMenu:Sub>
<twig:ContextMenu:Sub>
<twig:ContextMenu:SubTrigger>المزيد من الأدوات</twig:ContextMenu:SubTrigger>
<twig:ContextMenu:SubContent class="w-44">
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>حفظ الصفحة...</twig:ContextMenu:Item>
<twig:ContextMenu:Item>إنشاء اختصار...</twig:ContextMenu:Item>
<twig:ContextMenu:Item>تسمية النافذة...</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>أدوات المطور</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item variant="destructive">حذف</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:SubContent>
</twig:ContextMenu:Sub>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:CheckboxItem checked>إظهار الإشارات المرجعية</twig:ContextMenu:CheckboxItem>
<twig:ContextMenu:CheckboxItem>إظهار عناوين URL الكاملة</twig:ContextMenu:CheckboxItem>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:RadioGroup value="pedro">
<twig:ContextMenu:Label>الأشخاص</twig:ContextMenu:Label>
<twig:ContextMenu:RadioItem value="pedro" checked>Pedro Duarte</twig:ContextMenu:RadioItem>
<twig:ContextMenu:RadioItem value="colm">Colm Tuite</twig:ContextMenu:RadioItem>
</twig:ContextMenu:RadioGroup>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
<div dir="rtl">
<twig:ContextMenu>
<twig:ContextMenu:Trigger class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
<span class="hidden pointer-fine:inline-block">לחץ לחיצה ימנית כאן</span>
<span class="hidden pointer-coarse:inline-block">לחץ לחיצה ארוכה כאן</span>
</twig:ContextMenu:Trigger>
<twig:ContextMenu:Content class="w-48">
<twig:ContextMenu:Group>
<twig:ContextMenu:Sub>
<twig:ContextMenu:SubTrigger>ניווט</twig:ContextMenu:SubTrigger>
<twig:ContextMenu:SubContent class="w-44">
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>
<twig:ux:icon name="lucide:arrow-left" />
חזור
<twig:ContextMenu:Shortcut>⌘[</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item disabled>
<twig:ux:icon name="lucide:arrow-right" />
קדימה
<twig:ContextMenu:Shortcut>⌘]</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
<twig:ContextMenu:Item>
<twig:ux:icon name="lucide:rotate-cw" />
רענן
<twig:ContextMenu:Shortcut>⌘R</twig:ContextMenu:Shortcut>
</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:SubContent>
</twig:ContextMenu:Sub>
<twig:ContextMenu:Sub>
<twig:ContextMenu:SubTrigger>כלים נוספים</twig:ContextMenu:SubTrigger>
<twig:ContextMenu:SubContent class="w-44">
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>שמור עמוד...</twig:ContextMenu:Item>
<twig:ContextMenu:Item>צור קיצור דרך...</twig:ContextMenu:Item>
<twig:ContextMenu:Item>שם חלון...</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item>כלי מפתח</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:Item variant="destructive">מחק</twig:ContextMenu:Item>
</twig:ContextMenu:Group>
</twig:ContextMenu:SubContent>
</twig:ContextMenu:Sub>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:CheckboxItem checked>הצג סימניות</twig:ContextMenu:CheckboxItem>
<twig:ContextMenu:CheckboxItem>הצג כתובות URL מלאות</twig:ContextMenu:CheckboxItem>
</twig:ContextMenu:Group>
<twig:ContextMenu:Separator />
<twig:ContextMenu:Group>
<twig:ContextMenu:RadioGroup value="pedro">
<twig:ContextMenu:Label>אנשים</twig:ContextMenu:Label>
<twig:ContextMenu:RadioItem value="pedro" checked>Pedro Duarte</twig:ContextMenu:RadioItem>
<twig:ContextMenu:RadioItem value="colm">Colm Tuite</twig:ContextMenu:RadioItem>
</twig:ContextMenu:RadioGroup>
</twig:ContextMenu:Group>
</twig:ContextMenu:Content>
</twig:ContextMenu>
</div>
</div>
Accessibility
ContextMenu:Contentrendersrole="menu"and its entriesrole="menuitem",role="menuitemcheckbox"orrole="menuitemradio", witharia-checkedkept in sync by the controller.- The menu opens on right click, on the Menu key or
Shift+F10when the trigger has focus, and on a long press on touch screens. ArrowDownandArrowUpmove between items and wrap around,HomeandEndjump to the first and last, andEscapecloses the menu. Disabled items are skipped.- On a submenu trigger,
ArrowRight,EnterorSpacemoves the focus to the first item of the submenu (ArrowLeftinstead ofArrowRightin right-to-left layouts). Inside a submenu,ArrowLeft(ArrowRightin right-to-left layouts) moves the focus back to its trigger, and the arrow keys,HomeandEndstay within the submenu's own items. ContextMenu:Separatorrendersrole="separator", andContextMenu:Grouprendersrole="group".- A disabled item renders
aria-disabled="true"rather than being removed, so its presence stays discoverable. - The trigger is not a focusable element by default. Give it
tabindex="0"when keyboard users must be able to open the menu, and give the menu an accessible name witharia-labelonContextMenu:Content.
API Reference
<twig:ContextMenu>
| Block | Description |
|---|---|
content |
A ContextMenu:Trigger and a ContextMenu:Content. |
<twig:ContextMenu:CheckboxItem>
| Prop | Type | Default |
|---|---|---|
checked Whether the checkbox is initially checked.
|
boolean |
false |
disabled Whether the item is disabled.
|
boolean |
false |
closeOnSelect Whether selecting the item closes the menu.
|
boolean |
false |
| Block | Description |
|---|---|
content |
The label text. |
<twig:ContextMenu:Content>
| Prop | Type | Default |
|---|---|---|
side Which side of the pointer the menu appears on.
|
'top'|'right'|'bottom'|'left' |
'bottom' |
| Block | Description |
|---|---|
content |
The menu items, e.g. ContextMenu:Item, ContextMenu:Separator. |
<twig:ContextMenu:Group>
| Block | Description |
|---|---|
content |
One or more ContextMenu:Item components. |
<twig:ContextMenu:Item>
| Prop | Type | Default |
|---|---|---|
variant The visual style variant.
|
'default'|'destructive' |
'default' |
disabled Whether the item is disabled.
|
boolean |
false |
inset Whether the item is inset with extra start padding.
|
boolean |
false |
closeOnSelect Whether selecting the item closes the menu.
|
boolean |
true |
| Block | Description |
|---|---|
content |
The item label. |
<twig:ContextMenu:Label>
| Prop | Type | Default |
|---|---|---|
inset Whether the label is inset with extra start padding.
|
boolean |
false |
| Block | Description |
|---|---|
content |
The label text. |
<twig:ContextMenu:RadioGroup>
| Prop | Type | Default |
|---|---|---|
value The currently selected value.
|
string|null |
null |
| Block | Description |
|---|---|
content |
One or more ContextMenu:RadioItem components. |
<twig:ContextMenu:RadioItem>
| Prop | Type | Default |
|---|---|---|
value The value submitted when this radio is selected.
|
string |
- |
checked Whether this radio is initially checked.
|
boolean |
false |
disabled Whether the item is disabled.
|
boolean |
false |
closeOnSelect Whether selecting the item closes the menu.
|
boolean |
false |
| Block | Description |
|---|---|
content |
The label text. |
<twig:ContextMenu:Shortcut>
| Block | Description |
|---|---|
content |
The shortcut label, e.g. "⌘K". |
<twig:ContextMenu:Sub>
| Prop | Type | Default |
|---|---|---|
openDelay Delay in milliseconds before showing the sub-menu.
|
number |
0 |
closeDelay Delay in milliseconds before hiding the sub-menu.
|
number |
0 |
| Block | Description |
|---|---|
content |
A ContextMenu:SubTrigger and a ContextMenu:SubContent. |
<twig:ContextMenu:SubContent>
| Block | Description |
|---|---|
content |
The sub-menu items, e.g. ContextMenu:Item. |
<twig:ContextMenu:SubTrigger>
| Prop | Type | Default |
|---|---|---|
inset Whether the trigger is inset with extra start padding.
|
boolean |
false |
| Block | Description |
|---|---|
content |
The sub-menu trigger label. |
<twig:ContextMenu:Trigger>
| Block | Description |
|---|---|
content |
The region that opens the menu on right click or long press. |