Contextmenu #

Introduction
#

Fuz provides a customizable contextmenu that overrides the system contextmenu to provide helpful capabilities to users. Popular websites with similar features include Google Docs and Discord. Below are caveats about this breaking some user expectations, and a workaround for iOS Safari support. See also the contextmenu_event docs and w3 spec.

When you rightclick inside a ContextmenuRoot, or longpress on touch devices, it searches the DOM tree for behaviors defined with Contextmenu starting from the target element up to the root. If any behaviors are found, the Fuz contextmenu opens, showing all contextually available actions. If no behaviors are found, the default system contextmenu opens.

Here's a ContextmenuRoot with a Contextmenu inside another Contextmenu:

alert A -- rightclick or longpress here to open the contextmenu

alert B -- also inherits A

view code

This simple hierarchical pattern gives users the full contextual actions by default -- not just the actions for the target being clicked, but all ancestor actions too. This means users don't need to hunt for specific parent elements to find the desired action, unlike many systems -- instead, all actions in the tree are available, improving UX convenience and predictability at the cost of more noisy menus. Developers can opt out of this inheritance behavior by simply not nesting Contextmenu declarations, and submenus are useful for managing complexity.

Mouse and keyboard:

  • rightclick opens the Fuz contextmenu and not the system contextmenu (minus current exceptions for input/textarea/contenteditable)
  • holding Shift opens the system contextmenu, bypassing the Fuz contextmenu
  • keyboard navigation and activation should work similarly to the W3C APG menubar pattern

Touch devices:

  • longpress opens the Fuz contextmenu and not the system contextmenu (minus current exceptions for input/textarea/contenteditable)
  • tap-then-longpress opens the system contextmenu or performs other default behavior like selecting text, bypassing the Fuz contextmenu
  • tap-then-longpress can't work for clickable elements like links; longpress on the first contextmenu entry for those cases (double-longpress)

All devices:

  • opening the contextmenu on the contextmenu itself shows the system contextmenu, bypassing the Fuz contextmenu
  • opening the contextmenu attempts haptic feedback with navigator.vibrate

Selected root component:

Basic usage
#

Try opening the contextmenu on this panel with rightclick or tap-and-hold.

<ContextmenuRoot scoped> <Contextmenu> {#snippet entries()} <ContextmenuEntry run={() => (greeted = !greeted)}> Hello world <!-- false --> </ContextmenuEntry> <ContextmenuEntry run={() => (greeted_icon_snippet = !greeted_icon_snippet)}> {#snippet icon()}🌞{/snippet} Hello with an optional icon snippet <!-- false --> </ContextmenuEntry> <ContextmenuEntry run={() => (greeted_icon_string = !greeted_icon_string)} icon="🌚"> Hello with an optional icon string <!-- false --> </ContextmenuEntry> {/snippet} ...markup with the above contextmenu behavior... </Contextmenu> ...markup with only default contextmenu behavior... </ContextmenuRoot> ...markup without contextmenu behavior...
greeted = false
greeted_icon_snippet = false
greeted_icon_string = false

Default behaviors
#

<ContextmenuRoot scoped> ...<a href="https://www.fuz.dev/"> a link like this one </a>... </ContextmenuRoot>

Opening the contextmenu on a link like this one has special behavior by default. To accesss your browser's normal contextmenu, open the contextmenu on the link inside the contextmenu itself or hold Shift.

Although disruptive to default browser behavior, this allows links to have contextmenu behaviors, and it allows you to open the contextmenu anywhere to access all contextual behaviors.

Select text
#

When the Fuz contextmenu opens and the user has selected text, the menu includes a copy text entry.

Try and then opening the contextmenu on it.

Opening the contextmenu on an input or textarea opens the browser's default contextmenu.

contenteditable likewise has your browser's default contextmenu behavior.

contenteditable

contenteditable="plaintext-only"

Disable default behaviors
#

Check the boxes below to disable automatic a link detection and copy text detection, and see how the contextmenu behaves.

<ContextmenuRoot>

Try and opening the contextmenu in this panel.

Try opening the contextmenu on this link.

When no behaviors are defined, the system contextmenu is shown instead.

Expected: the Fuz contextmenu will open and show:

  • custom "some custom entry" entry
  • "copy text" entry when text is selected
  • link entry when clicking on a link

Custom instance
#

The ContextmenuRoot prop contextmenu accepts a custom ContextmenuState instance, allowing you to observe its reactive state and control it programmatically.

const contextmenu = new ContextmenuState(); <ContextmenuRoot {contextmenu} scoped>...</ContextmenuRoot>

Try opening the contextmenu on this panel, then use the navigation buttons below to cycle through entries -- just like the arrow keys. The color entries return {ok: true, close: false} to keep the menu open after activation, allowing you to select multiple colors using the activate button (↵).

Reactive state:

  • contextmenu.opened === false
  • contextmenu.x === 0 && contextmenu.y === 0

Full example
#

🏠
😺Alyssa
😸Ben
🌄
View example source

iOS compatibility
#

Fuz provides two versions of the contextmenu root component with different tradeoffs due to iOS Safari not supporting the contextmenu event as of October 2025, see WebKit bug #213953.

Use ContextmenuRoot by default for better performance and haptic feedback. Use ContextmenuRootForSafariCompatibility only if you need iOS Safari support.

ContextmenuRoot

  • standard, default implementation
  • relies on the browser's contextmenu event
  • much simpler, better performance with fewer and less intrusive event handlers, fewer edge cases that can go wrong
  • does not work on iOS Safari until WebKit bug #213953 is fixed

ContextmenuRootForSafariCompatibility

  • opt-in for iOS
  • some browsers (including mobile Chrome) block navigator.vibrate haptic feedback due to the timeout-based gesture detection (because it's not a direct user action)
  • implements custom longpress detection to work around iOS Safari's lack of contextmenu event support
  • works on all devices including iOS Safari
  • more complex implementation with custom touch event handling and gesture detection
  • a longpress is cancelled if you move the touch past a threshold before it triggers
  • opt into this version only if you need iOS Safari support

Selected root component:

Caveats
#

The Fuz contextmenu provides powerful app-specific UX, but it breaks from normal browser behavior by replacing the system contextmenu.

To mitigate the downsides:

  • The Fuz contextmenu only replaces the system contextmenu when the DOM tree has defined behaviors. Note that a links have default contextmenu behaviors unless disabled. Other interactive elements may have default behaviors added in the future.
  • The Fuz contextmenu does not open on elements that allow clipboard pasting like inputs, textareas, and contenteditables -- however this may change for better app integration, or be configurable.
  • To bypass on devices with a keyboard, hold Shift while rightclicking.
  • To bypass on touch devices (e.g. to select text), use tap-then-longpress instead of longpress.
  • Triggering the contextmenu inside the Fuz contextmenu shows the system contextmenu.

See also the contextmenu_event docs and the w3 spec.