dist/plugins/desktop-multiwindow-tauri/skills/desktop-multiwindow-tauri/SKILL.md
Tauri 2.x multi-window creation, event system, window state persistence, parent/child and modal windows
npx skillsauth add agents-inc/skills desktop-multiwindow-tauriInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Create windows from JS with
new WebviewWindow(label, options)or from Rust withWebviewWindowBuilder. Communicate across windows using events:emit()broadcasts globally,emitTo(label, event, payload)targets a specific window,emit_filter()targets multiple windows by predicate. Always call the unlisten function returned bylisten(). Usetauri-plugin-window-stateto persist window position/size across sessions. Window labels must be unique and are used for both event targeting and permission scoping.Current version: Tauri 2.x (stable). Multi-webview in a single window requires the
unstablefeature flag.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST give every window a unique label -- labels identify windows for event targeting, permissions, and retrieval)
(You MUST always call the unlisten function returned by listen() / once() -- leaked listeners cause memory leaks in long-running apps)
(You MUST check if a window already exists before creating it -- duplicate labels cause runtime errors)
(You MUST add window labels to capability file windows array -- windows without permissions cannot use plugins or core APIs)
(You MUST use @tauri-apps/api/event for global events and @tauri-apps/api/webviewWindow for window-scoped events -- mixing them causes missed events)
</critical_requirements>
Auto-detection: WebviewWindow, WebviewWindowBuilder, emit_to, emitTo, emit_filter, EventTarget, window label, multi-window, onCloseRequested, tauri-plugin-window-state, parent window, modal window, WebviewBuilder, add_child, getCurrentWebviewWindow, window.listen, window.emit, cross-window communication
When to use:
When NOT to use:
Key patterns covered:
Detailed resources:
Tauri's multi-window architecture is built on two key concepts: window labels for identification and events for communication.
Every window has a unique string label assigned at creation. This label is used everywhere: event targeting with emitTo, permission scoping in capability files, and retrieval with app.get_webview_window(label). Labels are the window's address.
The event system follows a pub-sub model with three tiers of targeting:
emit) -- all listeners in all windows receive the eventemitTo) -- only listeners in the specified window receive the eventemit_filter) -- listeners matching a predicate receive the eventWhen to use multi-window patterns:
When NOT to use multi-window:
Create windows from JavaScript or Rust. Always check if the window exists first to avoid duplicate-label errors.
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
const SETTINGS_WINDOW_LABEL = "settings";
const SETTINGS_WIDTH = 600;
const SETTINGS_HEIGHT = 400;
// Check if already open, focus it instead of creating duplicate
const existing = await WebviewWindow.getByLabel(SETTINGS_WINDOW_LABEL);
if (existing) {
await existing.setFocus();
return;
}
const settingsWindow = new WebviewWindow(SETTINGS_WINDOW_LABEL, {
url: "settings.html",
title: "Settings",
width: SETTINGS_WIDTH,
height: SETTINGS_HEIGHT,
resizable: false,
center: true,
});
settingsWindow.once("tauri://created", () => {
console.log("Settings window created");
});
settingsWindow.once("tauri://error", (e) => {
console.error("Failed to create window:", e);
});
Why good: checks for existing window first, uses named constants for dimensions, handles both creation success and error events
See examples/core.md for Rust-side creation with WebviewWindowBuilder and a full reusable helper.
Three tiers of event emission -- choose the narrowest scope that fits.
import { emit, emitTo, listen } from "@tauri-apps/api/event";
// Global: all windows receive
await emit("theme-changed", { theme: "dark" });
// Targeted: only the "editor" window receives
await emitTo("editor", "file-opened", { path: "/docs/readme.md" });
use tauri::{Emitter, EventTarget};
// Targeted: send to one window
app.emit_to("editor", "file-opened", payload)?;
// Filtered: send to multiple specific windows
app.emit_filter("file-opened", payload, |target| match target {
EventTarget::WebviewWindow { label } =>
label == "main" || label == "file-viewer",
_ => false,
})?;
Key decision: Use emit() for app-wide broadcasts (theme changes, user logout). Use emitTo() when you know the exact target window. Use emit_filter() when targeting multiple specific windows.
See examples/core.md for the full listening pattern with cleanup and cross-window state sync.
Intercept the close request to show a confirmation dialog (e.g., unsaved changes).
import { getCurrentWindow } from "@tauri-apps/api/window";
const unlisten = await getCurrentWindow().onCloseRequested(async (event) => {
if (hasUnsavedChanges()) {
event.preventDefault();
// Show your confirmation UI, then call window.close() or window.destroy() if confirmed
}
});
// Clean up listener when no longer needed
unlisten();
Key point: event.preventDefault() stops the window from closing. You must then either close it programmatically when the user confirms or leave it open. destroy() force-closes without triggering close-requested again.
See examples/core.md for the Rust-side equivalent using on_window_event.
Use tauri-plugin-window-state to automatically save and restore window position/size across sessions.
tauri::Builder::default()
.setup(|app| {
#[cfg(desktop)]
app.handle().plugin(
tauri_plugin_window_state::Builder::default().build()
);
Ok(())
})
Key point: Set visible: false in window config and let the plugin show the window after restoring state -- prevents a flash of the default position before restore. See examples/persistence.md for manual save/restore and StateFlags.
Create owned windows that are tied to a parent. Modal windows block interaction with the parent until closed.
import { WebviewWindow, getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
const DIALOG_LABEL = "confirm-dialog";
const DIALOG_WIDTH = 400;
const DIALOG_HEIGHT = 200;
const parent = getCurrentWebviewWindow();
const dialog = new WebviewWindow(DIALOG_LABEL, {
url: "confirm.html",
title: "Confirm Action",
width: DIALOG_WIDTH,
height: DIALOG_HEIGHT,
parent: parent,
center: true,
});
use tauri::WebviewWindowBuilder;
const DIALOG_WIDTH: f64 = 400.0;
const DIALOG_HEIGHT: f64 = 200.0;
WebviewWindowBuilder::new(&app, "confirm-dialog", tauri::WebviewUrl::App("confirm.html".into()))
.title("Confirm Action")
.inner_size(DIALOG_WIDTH, DIALOG_HEIGHT)
.parent(&parent_window)?
.center()
.build()?;
Key point: On macOS, parent makes the child a child window. On Linux, it makes it transient. Owned windows are hidden when the parent is minimized. See examples/advanced.md for modal patterns.
Create multiple webviews within a single window for panel/splitter layouts. Requires the unstable Cargo feature.
// Cargo.toml: tauri = { version = "2", features = ["unstable"] }
use tauri::{LogicalPosition, LogicalSize};
use tauri::webview::WebviewBuilder;
use tauri::window::WindowBuilder;
let window = WindowBuilder::new(&app, "main")
.inner_size(1200.0, 800.0)
.build()?;
let sidebar_width = 300.0;
let main_width = 900.0;
let height = 800.0;
window.add_child(
WebviewBuilder::new("sidebar", tauri::WebviewUrl::App("sidebar.html".into())),
LogicalPosition::new(0.0, 0.0),
LogicalSize::new(sidebar_width, height),
)?;
window.add_child(
WebviewBuilder::new("content", tauri::WebviewUrl::App("content.html".into())),
LogicalPosition::new(sidebar_width, 0.0),
LogicalSize::new(main_width, height),
)?;
Key point: This is behind the unstable feature flag and is desktop-only. Each child webview gets its own label and can communicate via the event system. See examples/advanced.md for resizing patterns.
<decision_framework>
Who should receive this event?
|-- ALL windows in the app?
| +-- emit() (global broadcast)
|-- ONE specific window?
| +-- emitTo(label, event, payload) (targeted)
|-- MULTIPLE specific windows (but not all)?
| +-- emit_filter() with predicate (Rust only)
+-- The backend (Rust side)?
+-- emit() from frontend + app.listen() in Rust
Does this UI surface need to be a separate window?
|-- Is it a settings/preferences panel?
| +-- YES: separate window with its own label
|-- Is it a modal confirmation dialog?
| +-- YES: child window with parent set
|-- Is it a dockable/undockable panel (IDE-style)?
| +-- YES: multi-webview in single window (unstable)
|-- Is it content that can live in the same DOM?
| +-- NO: don't create a new window -- use in-app routing/tabs
+-- Does it need to persist across window close?
+-- YES: use tauri-plugin-window-state
Where does window creation belong?
|-- Triggered by user action in UI (button click)?
| +-- Frontend: new WebviewWindow(label, options)
|-- Triggered by backend logic (file watcher, system event)?
| +-- Rust: WebviewWindowBuilder::new(app, label, url)
|-- Need multiple webviews in one window?
| +-- Rust: WindowBuilder + window.add_child(WebviewBuilder)
+-- Declared at startup in config?
+-- tauri.conf.json "windows" array
See reference.md for the complete API quick reference.
</decision_framework>
<red_flags>
High Priority Issues:
getByLabel() or app.get_webview_window()listen() returns an unlisten function; not calling it leaks memory, especially in long-running apps or components that mount/unmountwindows array -- the new window silently lacks permissions for plugins and core APIs@tauri-apps/api/tauri for event imports -- removed in v2; use @tauri-apps/api/event for global events and @tauri-apps/api/webviewWindow for window-scoped eventsemit() when you mean emitTo() -- broadcasting sensitive events (auth tokens, user data) to all windows is a security riskMedium Priority Issues:
tauri://error event on window creation -- creation can fail silently without this listenerclose() after preventDefault() in close handler -- triggers the close-requested event again; use destroy() to force-close without re-triggeringcenter: true or explicit position -- windows appear at OS-default position, which may be off-screen on multi-monitor setupsGotchas & Edge Cases:
onCloseRequested on dynamic windows: there is a known issue where event.preventDefault() may not work on windows created at runtime; use the Rust on_window_event approach as a fallbackserde::Serialize from Rust); complex types like functions or DOM nodes cannot be sent-, /, :, _ characters only -- no spaces or special characterscenter() centers on the primary monitor; for specific monitor positioning use setPosition() with PhysicalPositionemit_filter is Rust-only: there is no JavaScript equivalent; use multiple emitTo() calls from the frontendfeatures = ["unstable"] in Cargo.toml and is desktop-only (no mobile support)StateFlags::all() to include all state dimensions</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST give every window a unique label -- labels identify windows for event targeting, permissions, and retrieval)
(You MUST always call the unlisten function returned by listen() / once() -- leaked listeners cause memory leaks in long-running apps)
(You MUST check if a window already exists before creating it -- duplicate labels cause runtime errors)
(You MUST add window labels to capability file windows array -- windows without permissions cannot use plugins or core APIs)
(You MUST use @tauri-apps/api/event for global events and @tauri-apps/api/webviewWindow for window-scoped events -- mixing them causes missed events)
Failure to follow these rules will cause runtime errors, memory leaks, silent permission failures, and missed events.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events