Tauri アプリは中身が WebView なので、そのままだと右クリックで「再読み込み」「検証」といったブラウザのメニューが出たり、ボタンのラベルがドラッグで青く選択されたり、画像をつまんでデスクトップに落とせたりします。これらを止めてネイティブアプリらしい手触りにする方法をまとめます。JS / CSS だけでも実現できますが、確実に効かせるには Rust 側の initialization_script で HTML より先に注入するのが堅実です。開発中は右クリックの「検証」や文字選択が便利なので、本番ビルドだけ無効化します。プラグインや権限の追加は不要です。
1. フロントエンドから実装する (TypeScript)
右クリック・ドラッグ・ショートカットを止める
contextmenu を無条件に preventDefault() すると入力欄で「貼り付け」ができなくなるため、入力系の要素は素通しにします。
// src/native-feel.ts
const isEditable = (t: EventTarget | null) =>
t instanceof HTMLElement && (t.isContentEditable || t.closest('input, textarea, [contenteditable]') !== null);
export function disableWebFeel() {
if (!import.meta.env.PROD) return; // 本番ビルドだけ(Vite の場合)
document.addEventListener('contextmenu', (e) => { if (!isEditable(e.target)) e.preventDefault(); });
document.addEventListener('dragstart', (e) => { if (!isEditable(e.target)) e.preventDefault(); });
document.addEventListener('keydown', (e) => {
const k = e.key.toLowerCase();
const mod = e.ctrlKey || e.metaKey;
if (
e.key === 'F5' || e.key === 'F3' || // 再読み込み・検索
(mod && ['r', 'p', 'f', 'u', 's'].includes(k)) || // reload / print / find / source / save
(mod && e.shiftKey && k === 'i') // 開発者ツール
) e.preventDefault();
});
}
Ctrl+F をアプリ内検索に使うなら、preventDefault() の後に自前の検索 UI を開けば、ブラウザの検索バーだけが出なくなります。
テキスト選択と画像ドラッグを CSS で止める
html {
user-select: none;
-webkit-user-select: none; /* macOS / Linux の WebKit 系 */
cursor: default; /* 文字の上で I ビームにしない */
}
/* 入力欄と「コピーさせたい」テキストは選択可能に戻す */
input, textarea, [contenteditable], .selectable {
user-select: text; -webkit-user-select: text; cursor: text;
}
/* 画像・SVG をドラッグでつまめないようにする */
img, svg { -webkit-user-drag: none; }
エラーメッセージやライセンスキーなど、コピーさせたい場所には .selectable を付けます。全面 user-select: none のまま出荷して「エラー文をコピーできない」と言われるのが一番よくある失敗です。
2. バックエンドから実装する (Rust)
フロントエンドの JS はページ読み込み後に動くため、JS がエラーで止まった場合や読み込み中の一瞬は右クリックが効いてしまいます。initialization_script で注入したスクリプトは「グローバルオブジェクト生成後、HTML 解析前」に実行され、ページ遷移のたびに再実行されるので、この隙間がありません。tauri.conf.json の app.windows に書いたウィンドウは setup より前に作られるため、"windows": [] にしてウィンドウ作成を Rust に移します。
// src-tauri/src/lib.rs
use tauri::{WebviewUrl, WebviewWindowBuilder};
const NATIVE_FEEL_JS: &str = r#"
(() => {
if (!/^(tauri:\/\/|http:\/\/tauri\.)/.test(location.origin)) return; // 自アプリの origin だけ
const editable = (t) => t instanceof HTMLElement &&
(t.isContentEditable || t.closest('input, textarea, [contenteditable]'));
document.addEventListener('contextmenu', (e) => { if (!editable(e.target)) e.preventDefault(); });
document.addEventListener('dragstart', (e) => { if (!editable(e.target)) e.preventDefault(); });
document.addEventListener('keydown', (e) => {
const k = e.key.toLowerCase(), mod = e.ctrlKey || e.metaKey;
if (e.key === 'F5' || e.key === 'F3' || (mod && 'rpfus'.includes(k)) || (mod && e.shiftKey && k === 'i')) e.preventDefault();
});
document.addEventListener('DOMContentLoaded', () => {
const s = document.createElement('style');
s.textContent = 'html{user-select:none;-webkit-user-select:none;cursor:default}' +
'input,textarea,[contenteditable],.selectable{user-select:text;-webkit-user-select:text;cursor:text}' +
'img,svg{-webkit-user-drag:none}';
document.head.appendChild(s);
});
})();
"#;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let mut builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.title("My App")
.inner_size(900.0, 640.0);
#[cfg(not(debug_assertions))] // リリースビルドだけ注入
{
builder = builder.initialization_script(NATIVE_FEEL_JS);
}
builder.build()?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
冒頭の origin チェックは、外部サイトを同じウィンドウで開いたときにスクリプトを走らせないためのもので、docs.rs でも推奨されています(本番の origin は Windows が http://tauri.localhost、macOS / Linux が tauri://localhost)。iframe 内にも効かせるなら initialization_script_for_all_frames を使います。ウィンドウ作成を Rust に移したくなければ app.get_webview_window("main") に eval(NATIVE_FEEL_JS) する手もありますが、読み込み済みページにしか効かず、ページ遷移で消えます。
動作確認
npm run tauri dev では開発ビルドなので右クリックも選択も生きています(意図どおり)。npm run tauri build で生成した実行ファイルを起動し、次を確認します。
- 本文で右クリックしてもメニューが出ず、
<input>の上では「貼り付け」を含むメニューが出る。 - ボタンのラベルをドラッグしても青くならず、
.selectableの要素だけ選択できる。 - 画像をデスクトップへドラッグしてもコピーされない。
- F5 / Ctrl+R で画面が真っ白にならず、Ctrl+P で印刷ダイアログが出ない。
よくあるエラーと対処法
initialization_script を書いたのに効かない
tauri.conf.json の app.windows に同じラベルが残っていると、設定から作られたウィンドウが先に表示され、setup の build() はラベル重複でエラーになります。app.windows を空にしてください。
入力欄に貼り付け・コピーができない
contextmenu を無条件に止めているか、user-select: none を input にまで当てています。入力系要素を例外にしてください。
Property 'env' does not exist on type 'ImportMeta' という趣旨の型エラー
src/vite-env.d.ts に /// <reference types="vite/client" /> を書くか、tsconfig.json の compilerOptions.types に "vite/client" を追加します。
外部からのファイルドロップも効かなくなった
dragstart の抑止はアプリ「から」のドラッグだけを止めるので無関係です。Tauri のドラッグ&ドロップ処理と HTML5 の drop イベントの競合が原因で、HTML5 側で処理したいならウィンドウ設定の dragDropEnabled を false にします。
OS ごとの違いと注意点
- Windows (WebView2): 右クリックメニューに「更新」「開発者ツールで調査する」があり、F5 でページが再読み込みされて真っ白になります。無効化の効果が最も大きい環境です。
- macOS (WKWebView): Ctrl+クリックと 2 本指タップも
contextmenuとして届きます。CSS は-webkit-user-selectが必要で、ショートカットはe.metaKeyで判定します。 - Linux (webkit2gtk): 右クリックメニューに「Reload」「Inspect Element」があります。中クリック貼り付けは選択自体をなくせば発生しません。
- ショートカット抑止は WebView にフォーカスがあるときだけ効きます。Alt+F4 や Cmd+Q のような OS レベルの操作は止められません。
- ウィンドウをドラッグで動かす領域には
user-select: noneとdata-tauri-drag-regionを併用します。
