OS 標準のタイトルバーと枠を消した「フレームレス」ウィンドウにすると、タイトルバーまで HTML/CSS で自由にデザインできます。設定ファイルの decorations: false で作り、実行中は setDecorations() で切り替えます。ただし消えるのは見た目だけではありません。タイトルバーでの移動や、最小化・最大化・閉じるのボタンも無くなるので、それらを作り直すところまでがセットです。背景の透明化は 透明なウィンドウ、フレームレス時の影は 影を付ける・消す で扱います。
前提条件
プラグインは不要です。decorations: false 自体に権限は要りませんが、作り直す操作ごとに権限が必要で、多くは core:default に含まれません。
| 操作 | API | 権限 | core:default |
|---|---|---|---|
| ドラッグで移動 | data-tauri-drag-region / startDragging() | core:window:allow-start-dragging | 含まれない |
| ダブルクリックで最大化 | data-tauri-drag-region | core:window:allow-internal-toggle-maximize | 含まれる |
| 最小化 | minimize() | core:window:allow-minimize | 含まれない |
| 最大化・元に戻す | toggleMaximize() | core:window:allow-toggle-maximize | 含まれない |
| 閉じる | close() | core:window:allow-close | 含まれない |
| つまみでリサイズ | startResizeDragging() | core:window:allow-start-resize-dragging | 含まれない |
| 枠の付け外し | setDecorations() | core:window:allow-set-decorations | 含まれない |
isDecorated() と isMaximized() の読み取りは core:window:default に含まれます。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-close",
"core:window:allow-start-resize-dragging",
"core:window:allow-set-decorations"
]
}
title は画面に描かれなくなりますが、タスクバーなどに出る名前として使われるので残しておきます。
{
"app": {
"windows": [
{ "label": "main", "title": "My App", "width": 960, "height": 640, "decorations": false }
]
}
}
1. フロントエンドから実装する (TypeScript)
自作のタイトルバーを置く
<header class="titlebar" data-tauri-drag-region>
<span class="titlebar-title" data-tauri-drag-region>My App</span>
<div class="titlebar-buttons">
<button id="tb-min" type="button" aria-label="最小化">−</button>
<button id="tb-max" type="button" aria-label="最大化">□</button>
<button id="tb-close" type="button" aria-label="閉じる">✕</button>
</div>
</header>
<main class="content">本文</main>
.titlebar {
position: fixed;
inset: 0 0 auto 0;
height: 36px;
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 12px;
background: #20232a;
color: #fff;
user-select: none;
}
.titlebar-buttons button {
width: 46px;
height: 36px;
border: 0;
background: transparent;
color: inherit;
}
.titlebar-buttons button:hover { background: rgba(255, 255, 255, 0.12); }
#tb-close:hover { background: #e81123; }
.content { margin-top: 36px; }
body:not(.frameless) .titlebar { display: none; }
body:not(.frameless) .content { margin-top: 0; }
data-tauri-drag-region を付けた要素を押すとドラッグで移動でき、ダブルクリックで最大化と元に戻すが切り替わります。注意点は、この属性が付けた要素そのものにしか効かないことです。中の <span> を押しても動かないので、子要素にも属性を付けます。2.11 系では、親に data-tauri-drag-region="deep" と書けば子孫全体がドラッグ領域になります(効かない版では子に個別に付けます)。<button>、<a>、<input> などは、属性の付いた親の中にあっても自動でドラッグ対象から外れるので、普通にクリックできます。
import { getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
function onClick(id: string, action: () => Promise<void>) {
document.getElementById(id)?.addEventListener('click', () => {
action().catch((e) => console.error(e)); // 権限不足のエラーはここに出る
});
}
onClick('tb-min', () => win.minimize()); // core:window:allow-minimize
onClick('tb-max', () => win.toggleMaximize()); // core:window:allow-toggle-maximize
onClick('tb-close', () => win.close()); // core:window:allow-close
// ダブルクリックでも最大化が変わるので、リサイズのたびにボタンを合わせる
async function syncMaxButton() {
const btn = document.getElementById('tb-max');
if (!btn) return;
const maximized = await win.isMaximized();
btn.textContent = maximized ? '❐' : '□';
btn.setAttribute('aria-label', maximized ? '元に戻す' : '最大化');
}
document.body.classList.toggle('frameless', !(await win.isDecorated()));
void win.onResized(() => void syncMaxButton());
void syncMaxButton();
close() は標準の × ボタンと同じく閉じる前のイベントを発生させるので、閉じる前の確認 もそのまま使えます。メニューまで自作バーに入れるなら 自作のタイトルバーにメニューを組み込む、ドラッグ領域の細かい指定は ドラッグ移動できる領域を作る を参照してください。
右下にリサイズ用のつまみを置く
枠が無いと、どこをつかめばリサイズできるのか利用者から見えません。つまみを置き、押されたら startResizeDragging() を呼ぶと、そこからリサイズできます。
import { getCurrentWindow } from '@tauri-apps/api/window';
const grip = document.createElement('div');
grip.style.cssText = 'position:fixed;right:0;bottom:0;width:14px;height:14px;cursor:nwse-resize;';
document.body.append(grip);
grip.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
e.preventDefault();
// 'SouthEast' = 右下方向。core:window:allow-start-resize-dragging が必要
getCurrentWindow().startResizeDragging('SouthEast').catch((err) => console.error(err));
});
実行中に枠を付け外しする
「ミニ表示では枠を消す」のように切り替えるときは、標準の枠と自作バーを入れ替えます。上の CSS の .frameless クラスで自作バーの表示を切り替えています。
import { getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
export async function toggleFrame() {
const decorated = await win.isDecorated();
await win.setDecorations(!decorated); // core:window:allow-set-decorations
document.body.classList.toggle('frameless', decorated); // 枠を消したら自作バーを出す
}
2. バックエンドから実装する (Rust)
Rust では WebviewWindowBuilder の decorations(false) で作り、set_decorations() で切り替えます。下の例では、macOS だけ標準のボタン(信号機)を残すオーバーレイ型にしています(後述)。Windows ではウィンドウを同期コマンドで作ると固まることがあるので、作成するコマンドは async にします。
use tauri::{WebviewUrl, WebviewWindowBuilder};
/// フレームレスのサブウィンドウを開く
#[tauri::command]
async fn open_frameless(app: tauri::AppHandle) -> Result<(), String> {
let builder = WebviewWindowBuilder::new(&app, "player", WebviewUrl::App("player.html".into()))
.title("Player")
.inner_size(480.0, 270.0);
#[cfg(target_os = "macos")]
let builder = builder
.title_bar_style(tauri::TitleBarStyle::Overlay)
.hidden_title(true);
#[cfg(not(target_os = "macos"))]
let builder = builder.decorations(false);
builder.build().map_err(|e| e.to_string())?;
Ok(())
}
/// 呼び出し元ウィンドウの枠を切り替え、切り替え後の状態を返す
#[tauri::command]
fn toggle_decorations(window: tauri::WebviewWindow) -> Result<bool, String> {
let next = !window.is_decorated().map_err(|e| e.to_string())?;
window.set_decorations(next).map_err(|e| e.to_string())?;
Ok(next)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![open_frameless, toggle_decorations])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Rust のコマンド経由なら、JS 側に core:window:allow-set-decorations は要りません。
import { invoke } from '@tauri-apps/api/core';
const decorated = await invoke<boolean>('toggle_decorations');
document.body.classList.toggle('frameless', !decorated);
動作確認
npm run tauri dev で起動すると、標準のタイトルバーが消えて自作のバーだけが表示されます。バーの余白やタイトル文字をドラッグすると移動し、ダブルクリックで最大化と元に戻すが切り替わり、ボタンの記号も変わります。右下のつまみでリサイズでき、toggleFrame() を呼ぶと標準の枠が戻って自作バーが隠れます。
よくあるエラーと対処法
- ドラッグしても動かず、コンソールに「window.start_dragging not allowed. Permissions associated with this command: core:window:allow-start-dragging」: 権限の追加漏れです。リリースビルドでは「Command plugin:window|start_dragging not allowed by ACL」と出ます。
- タイトル文字の上だけドラッグできない: 属性が親にしか付いていません。子要素にも付けるか
"deep"を使います。 - ボタンを押しても何も起きない:
window.minimize not allowed.などで始まるエラーがコンソールに出ていれば、表の権限が足りません。 - ダブルクリックしても最大化しない:
resizable: falseかmaximizable: falseのウィンドウでは、ドラッグ領域のダブルクリックは何もしません。
OS ごとの違いと注意点
- 影: フレームレスにしたときの影の出方は OS で異なり、Windows では周りに細い白い線が付くことがあります。挙動と対処は 影を付ける・消す にまとめています。
- macOS:
decorations: falseの代わりに、titleBarStyle: "Overlay"とhiddenTitle: trueで標準のボタンを残したままタイトルバーの領域までページを広げる方法があります。この場合もドラッグ領域は自分で用意し、非アクティブなウィンドウはドラッグできない制限があります。タイトルバーの高さは macOS のバージョンで違うので、上端に余裕を持たせます。ボタンの位置はtrafficLightPosition(2.4 以降、decorations: trueと Overlay が条件)で調整できます。 - macOS だけ設定を変える:
src-tauri/tauri.macos.conf.jsonに書いた設定は macOS のときだけtauri.conf.jsonに重ねられます。windowsのような配列は丸ごと置き換わるので、ウィンドウの設定はすべて書き直します。
{
"app": {
"windows": [
{
"label": "main",
"title": "My App",
"width": 960,
"height": 640,
"titleBarStyle": "Overlay",
"hiddenTitle": true
}
]
}
}
