OS 標準のタイトルバーを消して独自ヘッダーを描くと、そのままではウィンドウを掴んで動かせません。Tauri では HTML 要素に data-tauri-drag-region 属性を付けるだけで、その要素上でのマウス押下 + 移動が OS ネイティブのウィンドウ移動になります。JS からタイミングを制御したいときは getCurrentWindow().startDragging() を直接呼べます。ここでは decorations: false の自作タイトルバーを題材に、ボタンをドラッグ対象から外す方法とダブルクリック最大化の挙動まで扱います。
前提条件
プラグインは不要です。core:window:allow-start-dragging は core:window:default に含まれていないので src-tauri/capabilities/default.json に追加します。最小化・最大化・閉じるボタンも自作するなら、それらの権限も入れます。
{
"permissions": [
"core:default",
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-close"
]
}
標準タイトルバーは tauri.conf.json の app.windows[] で消します。
{
"app": {
"windows": [
{ "label": "main", "decorations": false, "width": 900, "height": 600 }
]
}
}
1. フロントエンドから実装する (TypeScript)
属性方式(推奨)
data-tauri-drag-region は属性を付けた要素そのものにしか効き、子要素には継承されません。タイトル文字を <span> に入れるとその上では掴めなくなる一方、ボタンには属性を付けないだけで「ボタンはクリック、それ以外はドラッグ」と分離できます。
<header data-tauri-drag-region class="titlebar">
<span data-tauri-drag-region class="title">My App</span>
<div class="controls">
<button id="min">–</button>
<button id="max">□</button>
<button id="close">✕</button>
</div>
</header>
.titlebar {
height: 32px; display: flex; align-items: center; justify-content: space-between;
padding-left: 12px; background: #2b2b2b; color: #eee;
user-select: none; /* ドラッグ中の文字選択を防ぐ */
}
.controls button { width: 46px; height: 32px; border: 0; background: transparent; color: inherit; }
.controls button:hover { background: #444; }
import { getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
document.getElementById('min')!.addEventListener('click', () => win.minimize());
document.getElementById('max')!.addEventListener('click', () => win.toggleMaximize());
document.getElementById('close')!.addEventListener('click', () => win.close());
属性を付けた領域をダブルクリックすると最大化がトグルします。core:window:default に含まれる core:window:allow-internal-toggle-maximize が使われるので追加権限は不要です。resizable: false のウィンドウでは何も起きません。
startDragging() を JS から呼ぶ方式
属性が使えない要素(Canvas や、フレームワークが属性を書き換えるコンポーネント)や「Alt を押しながらならどこでもドラッグ」のような条件付きには、pointerdown で startDragging() を呼びます。左ボタン判定や修飾キー判定は自前で行います。
import { getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
document.getElementById('board')!.addEventListener('pointerdown', async (e) => {
if (e.button !== 0 || !e.altKey) return; // Alt + 左ボタンのときだけ
e.preventDefault();
if (e.detail === 2) await win.toggleMaximize(); // ダブルクリック相当
else await win.startDragging();
});
startDragging() はドラッグを開始するだけで、以降の移動と離す操作は OS が処理します。
2. バックエンドから実装する (Rust)
Rust 側には Window::start_dragging() があります。マウス押下は Webview 内で起きるので実用上は JS から呼びますが、コマンド経由にすると ACL の権限追加なしで動かせます。
#[tauri::command]
fn begin_drag(window: tauri::Window) -> Result<(), String> {
window.start_dragging().map_err(|e| e.to_string())
}
// invoke_handler に begin_drag を登録
import { invoke } from '@tauri-apps/api/core';
const el = document.querySelector<HTMLElement>('.titlebar')!; // 上の HTML のタイトルバー
el.addEventListener('pointerdown', (e) => { if (e.button === 0) invoke('begin_drag'); });
動作確認
npm run tauri dev で起動すると、タイトルバーのないウィンドウが出ます。ヘッダーの暗い帯を掴んで動かせること、3 つのボタンが最小化・最大化・閉じるとして働くこと、帯のダブルクリックで最大化がトグルすることを確認してください。span.title から属性を外すと、文字の上だけ掴めなくなるのが分かります。
よくあるエラーと対処法
- 動かず、コンソールに
window.start_dragging not allowed. Permissions associated with this command: core:window:allow-start-draggingという趣旨のエラー: 権限未追加です。追加後はtauri devを再起動します(capabilities はホットリロードされません)。 - タイトル文字やアイコンの上だけドラッグできない: 子要素に継承されないためです。掴ませたい子要素すべてに
data-tauri-drag-regionを付けます。 - ボタンをクリックするとウィンドウが動く: ボタン側にも属性が付いているか、React などの
{...props}展開で属性が伝播しています。ボタンとその祖先(div.controlsまで)から属性を外します。 - Electron の
-webkit-app-region: dragが効かない: Tauri はこの CSS を解釈しません。属性方式に置き換えます。 startDragging()をclickで呼んでいる:clickはボタンを離した後に発火します。pointerdown/mousedownで呼びます。
OS ごとの違いと注意点
- macOS:
decorations: falseにすると信号機ボタンも消えます。信号機を残したいならdecorations: trueのまま"titleBarStyle": "Overlay"と"hiddenTitle": trueにし、ヘッダー左端に約 70px の余白を取って重なりを避けます。 - Windows: 自作ボタンでは Windows 11 のスナップレイアウト(最大化ボタンのホバーメニュー)は出ません。影と角丸は
shadow設定(win-014)に依存します。 - Linux: X11 では属性方式・
startDragging()とも動作します。Wayland ではコンポジターが移動を担当するため環境で挙動が異なります。 - 属性を付けた領域内の
<input>は、属性を直接付けない限り通常どおり入力できます。
