入力フォームや確認画面をサブウィンドウで出し、閉じるまで親ウィンドウを操作させたくないことがあります。Tauri のウィンドウには「モーダルにする」という 1 つの設定は無いので、parent で親子関係を付けて親より手前に保つことと、親を setEnabled(false) で操作できなくすることを組み合わせます。大事なのは、どんな閉じ方をされても親を元に戻すことです。
サブウィンドウ用のページ(ここでは modal.html)の用意と Vite の設定は 新しいサブウィンドウを開く と同じです。ネイティブのダイアログや HTML の <dialog> で足りる場面も多いので、どれを使うかは 操作をブロックするモーダルダイアログにする で比べてください。
前提条件
プラグインは不要です。親(main)がウィンドウを作る権限、自分を無効・有効にする core:window:allow-set-enabled、閉じた後にフォーカスを戻す core:window:allow-set-focus を追加します。どれも core:default には含まれません。モーダル側は自分の「閉じる」ボタンで close() を呼ぶので、そのラベルも windows に入れます。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main and modal windows",
"windows": ["main", "modal"],
"permissions": [
"core:default",
"core:webview:allow-create-webview-window",
"core:window:allow-set-enabled",
"core:window:allow-set-focus",
"core:window:allow-close"
]
}
| 使うもの | 役割 |
|---|---|
作成時の parent | 親より手前に保つ(意味は OS ごとに違う。後述) |
親の setEnabled(false) | 親へのクリックやキー入力を受け付けなくする |
モーダルの tauri://destroyed | どの閉じ方でも親を有効に戻す合図にする |
1. フロントエンドから実装する (TypeScript)
親ウィンドウ側
親を無効にするのは作成に成功してからにします。先に無効にすると、作成に失敗したときに親が操作できないまま残ります。復帰は「閉じるボタン」ではなく、モーダルが破棄されたときの tauri://destroyed で行います。× ボタンや Alt+F4 で閉じられても、ここは必ず通ります。
関数はモーダルが閉じたときに解決する Promise を返すので、呼び出し側は await openModal() でダイアログのように待てます。
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
import { getCurrentWindow } from '@tauri-apps/api/window';
const LABEL = 'modal';
// モーダルが閉じられたら解決する
export async function openModal(): Promise<void> {
const parent = getCurrentWindow();
const existing = await WebviewWindow.getByLabel(LABEL);
if (existing) {
await existing.setFocus();
return;
}
const modal = new WebviewWindow(LABEL, {
url: '/modal.html',
title: '確認',
width: 420,
height: 220,
resizable: false,
minimizable: false,
center: true,
parent, // 親子関係を付ける(alwaysOnTop は付けない)
});
await new Promise<void>((resolve, reject) => {
void modal.once('tauri://created', () => resolve());
void modal.once<string>('tauri://error', (e) => reject(new Error(String(e.payload))));
});
// 閉じられ方に関係なく、破棄されたら親を戻す(登録を待ってから親を止める)
let onClosed!: () => void;
const closed = new Promise<void>((resolve) => (onClosed = resolve));
await modal.once('tauri://destroyed', async () => {
await parent.setEnabled(true);
await parent.setFocus(); // 別のアプリが前に出ることがあるので親に戻す
onClosed();
});
await parent.setEnabled(false);
return closed;
}
document.querySelector('#open-modal')?.addEventListener('click', async () => {
try {
await openModal();
console.log('モーダルが閉じられました');
} catch (e) {
console.error('モーダルを開けません:', e);
}
});
alwaysOnTop: true は付けません。これは親だけでなく他のアプリより手前にも居座る設定で、ブラウザに切り替えてもモーダルが上に残ります。親より手前に保つだけなら parent で足ります。
モーダル側
モーダル側は普通のサブウィンドウと同じで、閉じるだけです。入力内容を親に返すには、閉じる前に emitTo() で親へ送ります(別のウィンドウ作成時にデータを渡す)。
import { getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
for (const id of ['ok', 'cancel']) {
document.getElementById(id)?.addEventListener('click', () => void win.close());
}
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') void win.close(); // Esc でも閉じられるようにする
});
2. バックエンドから実装する (Rust)
JS の後始末は親のページが動いていることが前提です。開発中にファイルを保存してページが再読み込みされると、tauri://destroyed の処理ごと消え、親が無効のまま残ります。Rust で開いてウィンドウのイベントで戻すと、ページの状態に左右されません。JS 側のウィンドウ作成・setEnabled・setFocus の権限も要らなくなります。
コマンドの引数に tauri::WebviewWindow を書くと呼び出し元(親)が渡されます。Windows ではウィンドウを作るコマンドを同期にすると固まるので async にします。
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder, WindowEvent};
#[tauri::command]
async fn open_modal(parent: tauri::WebviewWindow) -> Result<(), String> {
let app = parent.app_handle().clone();
if let Some(existing) = app.get_webview_window("modal") {
return existing.set_focus().map_err(|e| e.to_string());
}
let modal = WebviewWindowBuilder::new(&app, "modal", WebviewUrl::App("modal.html".into()))
.title("確認")
.inner_size(420.0, 220.0)
.resizable(false)
.minimizable(false)
.center()
.parent(&parent)
.map_err(|e| e.to_string())?
.build()
.map_err(|e| e.to_string())?;
// 作成に成功してから親を止め、モーダルが破棄されたら戻す
parent.set_enabled(false).map_err(|e| e.to_string())?;
let owner = parent.clone();
modal.on_window_event(move |event| {
if let WindowEvent::Destroyed = event {
let _ = owner.set_enabled(true);
let _ = owner.set_focus();
}
});
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![open_modal])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
await invoke('open_modal'); // 作成が終わった時点で戻る(閉じるまでは待たない)
動作確認
npm run tauri dev で #open-modal のボタンを押すと、画面中央に「確認」ウィンドウが開きます。この間は親をクリックしても反応せず、別のアプリに切り替えるとモーダルも親と一緒に後ろへ回ります。OK・キャンセル・Esc・× のどれで閉じても親が操作できる状態に戻り、親のコンソールに「モーダルが閉じられました」と出ます。
親が止まっていることは、モーダル側のスクリプトに次を足すと確かめられます。isEnabled() の権限は core:default に含まれます。
import { Window } from '@tauri-apps/api/window';
const main = await Window.getByLabel('main');
console.log('main enabled:', await main?.isEnabled()); // モーダルを開いている間は false
よくあるエラーと対処法
- 「window.set_enabled not allowed. Permissions associated with this command: core:window:allow-set-enabled」: 権限の追加漏れです。リリースビルドでは「Command plugin:window|set_enabled not allowed by ACL」になります。
- 親が無効のまま戻らない: 親を無効にした後で例外が起きたか、親のページが再読み込みされて後始末が消えています。開発中に固まったらアプリを再起動し、確実さが必要なら Rust 版にします。
- 「window not found」でモーダルが開かない:
parentに渡したラベルのウィンドウがありません。ラベルの綴りか、親が既に閉じていないかを確かめます。 - モーダルが他のアプリより手前に残る:
alwaysOnTop: trueが付いています。外してparentだけにします。 - モーダルを閉じても親にフォーカスが戻らない:
setFocus()を呼んでいないか、core:window:allow-set-focusがありません。
OS ごとの違いと注意点
parent の意味は OS ごとに違います。
- Windows: 親が「オーナー」になります。モーダルは常に親より手前に表示され、親が最小化されると一緒に隠れ、親が破棄されると一緒に破棄されます。
- macOS: 親の子ウィンドウとして追加されます。
- Linux: 親に対する一時的なウィンドウ(transient)として扱われます。また
minimizable: falseは Linux では効きません。 - 共通: モーダル側で閉じる前の確認をする場合は ウィンドウを閉じる前に確認ダイアログを出す と組み合わせます。確認でキャンセルされたときは破棄されないので、親も無効のまま保たれます。
- 共通: 画面の一部を覆うだけで足りるなら、ウィンドウを増やさない HTML/CSS の自作ダイアログ の方が手軽です。
