Skip to the content.

Context menus

← Back to index

Needs the context-menu permission (low risk: it only adds entries to menus). The example also writes the clipboard, which is ctx.clipboard behind the clipboard permission — the user is asked the first time — because navigator.clipboard is not reachable through the source policy.

ctx.contextMenu.contribute((target) => [
  { kind: 'divider' },
  {
    kind: 'item',
    icon: 'content_copy',
    label: 'Copy element tag',
    onSelect: () => ctx.clipboard.writeText(target.element.tagName),
  },
  {
    kind: 'submenu',
    icon: 'tune',
    label: 'Mode',
    items: () => MODES.map((m) => ({
      kind: 'item' as const,
      icon: 'radio_button_checked',
      label: m.label,
      checked: ctx.settings.get('mode') === m.value,
      onSelect: () => void ctx.settings.set('mode', m.value),
    })),
  },
]);

Entry kinds

Kind Fields
item icon, label, onSelect, optional danger, checked
submenu icon, label, items() — resolved when opened, so it can reflect live state
divider none

Targeting

// Only on friend rows.
ctx.contextMenu.contributeFor('.friend-row', (target) => [ /* … */ ]);

target.entity is populated when a VRChat entity can be read off the clicked element or one of its ancestors. VRCNext carries ids in inline onclick handlers (openFriendDetail('usr_…'), navOpenModal('world','wrld_…')) and in data-* attributes (data-uid, data-wid, data-avid, data-gid, data-location, …); the host reads both, nearest ancestor first, and derives the type from the id prefix. An instance location (wrld_…:12345~…) resolves as { type: 'instance', id: location }.

if (target.entity?.type === 'user') {
  entries.push({ kind: 'item', icon: 'badge', label: 'Copy user id',
                 onSelect: () => ctx.clipboard.writeText(target.entity.id) });
}

Standalone menus

element.addEventListener('contextmenu', (event) => {
  event.preventDefault();
  ctx.contextMenu.open(event.clientX, event.clientY, entries);
});

How it works, and what that costs

VRCNext builds its menus inside a module closure — getMenuConfig is unreachable from outside. So contributions are appended to the rendered menu rather than merged into its item list: a capture-phase contextmenu listener records the target, and a MutationObserver appends plugin buttons once VRCNext has drawn its own.

Consequences worth knowing:

← VRCNext Bridge · OSC →