右クリック・テキスト選択・ドラッグを無効化して Web っぽさを消す

右クリックメニュー、テキスト選択、画像のドラッグ、ブラウザ既定のショートカット(F5・Ctrl+P など)を無効にして Web っぽさを消す。本番ビルドだけに適用する方法も示す。

フロントエンド 対象: Tauri 2.x 更新日: 読了目安: 約7分 front-011
目次
  1. 1. フロントエンドから実装する (TypeScript)
  2. 右クリック・ドラッグ・ショートカットを止める
  3. テキスト選択と画像ドラッグを CSS で止める
  4. 2. バックエンドから実装する (Rust)
  5. 動作確認
  6. よくあるエラーと対処法
  7. initialization_script を書いたのに効かない
  8. 入力欄に貼り付け・コピーができない
  9. Property 'env' does not exist on type 'ImportMeta' という趣旨の型エラー
  10. 外部からのファイルドロップも効かなくなった
  11. OS ごとの違いと注意点
  12. 関連レシピ

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 を併用します。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する