Completely Disabling the Native Browser Context Menu in Tauri Apps

In a Tauri v2 + React desktop app, right-clicking anywhere pops up the native Chromium context menu — Back, Forward, Reload, View Source, Inspect, and friends.

That shatters the native feel of the app, leaks implementation details (it’s a WebView under the hood), and conflicts with the app’s own custom context menus — transfer panel actions, terminal menus, and the like.

Root Cause

A Tauri frontend runs inside a WebView (WebKitGTK on macOS/Linux, WebView2 / Edge Chromium on Windows). A WebView is essentially an embedded browser, so its default behavior includes:

  1. Right-click fires the browser’s contextmenu event → native menu pops up
  2. Unlike Electron, there is no global config option to turn this off
  3. Every DOM element can become a right-click trigger point

Why CSS Can’t Fix It

1
2
/* ❌ CSS cannot stop the contextmenu event */
* { user-select: none; }

Properties like user-select only affect text selection — they cannot prevent the contextmenu event from firing or the browser menu from appearing. This has to be solved with JavaScript event handling.

The Fix

Core idea: intercept once at the app root

Attach an onContextMenu handler on the outermost root element of the React tree and call e.preventDefault() to suppress the browser default. Thanks to React’s event bubbling, no matter which child component gets right-clicked, the event bubbles up to the root and gets intercepted.

1
2
3
4
5
6
7
8
9
10
11
12
// App.tsx — application entry
export default function App() {
return (
<div className="app-layout" onContextMenu={(e) => e.preventDefault()}>
{/* everything in the app */}
<Sidebar />
<MainContent />
<ToolPanel />
{/* ... all dialogs, modals, panels ... */}
</div>
);
}

One line of code covers the entire app.

Key advantages

Aspect Detail
Coverage Every existing page/component — and every future one
Child-friendly Child components can still handle contextmenu themselves for custom menus
Low maintenance No need to sprinkle handlers component by component
Zero perf cost It’s just a trivial event interceptor

Handling multi-window apps

If the app has standalone windows (e.g. a detached SFTP file-manager window), each window has its own render tree and root element, so each needs its own handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// App.tsx — main window
return (
<div className="app-layout" onContextMenu={(e) => e.preventDefault()}>
{/* main window content */}
</div>
);

// App.tsx — standalone SFTP window (another return branch)
if (isSftpWindow && sftpWindowSessionId) {
return (
<div className="sftp-standalone-window" onContextMenu={(e) => e.preventDefault()}>
<SftpBrowser sessionId={sftpWindowSessionId} />
</div>
);
}

Coexisting with custom context menus

Regions that need their own right-click menu (file lists, table rows) can bind onContextMenu normally in the child component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// TransferPanel.tsx — custom context menu for the transfer list
<table>
{transfers.map(transfer => (
<tr
key={transfer.id}
// custom context menu handler
onContextMenu={(e) => {
e.preventDefault(); // suppress the browser menu
e.stopPropagation(); // optional: stop bubbling to the root
showCustomMenu(e, transfer); // show the custom menu
}}
>
...
</tr>
))}
</table>

{/* custom context menu component */}
<TransferContextMenu visible={...} position={...} onClose={...}>

Note: since the root already called e.preventDefault(), the one in the child is redundant but harmless. If you want only specific regions to suppress the browser menu (rather than globally), use e.stopPropagation() in the child to keep the event from bubbling further.

Full Implementation Checklist

Step 1: Intercept at the main-window root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/App.tsx
return (
<SyncScrollProvider>
<div className="app-layout" onContextMenu={(e) => e.preventDefault()}>
<NotificationToasts />
<TitleBar />
<div className="app-body">
<Sidebar />
<div className="app-main">
{/* ... */}
</div>
</div>
</div>
</SyncScrollProvider>
);

Step 2: Intercept at every standalone window root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/App.tsx — every standalone return branch needs it
if (isSftpWindow) {
return (
<div className="sftp-standalone-window" onContextMenu={(e) => e.preventDefault()}>
<SftpBrowser />
</div>
);
}

if (isOtherStandaloneWindow) {
return (
<div className="other-window-root" onContextMenu={(e) => e.preventDefault()}>
{/* ... */}
</div>
);
}

Step 3: Verify coverage

Right-click-test each of these areas one by one:

  • Home / welcome panel
  • Host list sidebar
  • Terminal area
  • Settings toolbox (all sub-tabs)
  • SFTP file browser (embedded and standalone window modes)
  • File transfer panel
  • Add-host dialog
  • Quick-connect dialog
  • Snippets panel
  • Config file picker dropdown
  • About dialog
  • Every other popup / modal

Common Misconceptions

Myth 1: Adding it to one component is enough

Wrong — dead wrong. New pages and dialogs get added all the time and are easy to miss. Solve it at the architecture level with unified handling at the root element.

Myth 2: CSS can solve it

1
2
3
4
/* ❌ This does nothing */
body {
-webkit-context-menu: none;
}

The -webkit-context-menu property is deprecated and unsupported by modern WebViews. JavaScript event handling is required.

Myth 3: It breaks copy/paste menus in inputs

No. e.preventDefault() only suppresses the native browser menu from popping up.

  • Text editing in <input> / <textarea> / [contenteditable] is unaffected
  • If you want to keep the native copy/paste/spellcheck menu inside inputs, add a condition:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div
onContextMenu={(e) => {
// let inputs keep the native browser menu
const target = e.target as HTMLElement;
if (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable
) {
return; // don't suppress
}
e.preventDefault();
}}
>

Myth 4: Tauri has a config switch for this

As of Tauri v2, there is no built-in option to disable the WebView context menu. It’s WebView-level behavior and must be handled in the web frontend layer.

Going Further: Fine-Grained Control

If the app needs tiered policies (different rules per region), use a Context + HOC approach:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/context/ContextMenuContext.tsx
const ContextMenuContext = createContext<{
disable: boolean;
}>({ disable: true });

// Browser menu disabled by default; sub-regions can opt back in
export function ContextMenuProvider({ children }: { children: React.ReactNode }) {
return (
<ContextMenuContext.Provider value={{ disable: true }}>
<div onContextMenu={(e) => {
if (e.currentTarget.getAttribute('data-native-menu') !== 'true') {
e.preventDefault();
}
}}>
{children}
</div>
</ContextMenuContext.Provider>
);
}

For most Tauri apps, though, one preventDefault() at the root is plenty — no need to over-engineer it.

Summary

Item Detail
Problem Tauri/WebView apps pop the native browser menu on right-click
Cause Default WebView behavior, no global switch to turn it off
Fix Root element onContextMenu={(e) => e.preventDefault()}
Cost 1 line of code
Coverage The whole app, including pages added in the future
Caveat Multi-window apps need it on each window’s root element

Applies to Tauri v1/v2 with React, Vue, Svelte, or any frontend framework.