How to Handle Multi-Window Applications in Tauri: A Complete Guide

Multi-window applications in Tauri are created by instantiating multiple Window objects with unique labels via WindowBuilder or WebviewWindowBuilder, where each window owns a webview and shares the same AppHandle for coordinated lifecycle management.

Managing multiple windows in the tauri-apps/tauri ecosystem requires understanding how the framework separates window management from webview rendering. Every top-level UI element operates as a window containing a single webview, orchestrated by the WindowManager that maintains a BTreeMap<WindowId, WindowWrapper> registry in crates/tauri/src/manager/mod.rs. This architecture allows developers to spawn independent OS windows or embed multiple webviews within a single window while maintaining a unified application state.

Understanding Tauri's Window Architecture

Tauri implements a three-layer architecture for window management:

Creating Top-Level Windows

Using WindowBuilder

The primary method for spawning independent OS windows is WindowBuilder::new, defined at line 1045 in crates/tauri/src/window/mod.rs. Each window requires a unique label that serves as its identifier throughout the application lifecycle.

tauri::Builder::default()
    .setup(|app| {
        // Primary window
        let _main = tauri::WindowBuilder::new(app, "main")
            .title("Main Application")
            .inner_size(1024.0, 768.0)
            .build()?;

        // Secondary window with distinct label
        let second = tauri::WindowBuilder::new(app, "settings")
            .title("Settings")
            .inner_size(400.0, 600.0)
            .build()?;

        Ok(())
    })
    .run(tauri::generate_context!())
    .expect("error running app");

The build() method at line 1065 dispatches the creation request to the runtime via RuntimeHandle::create_window. Every label must be unique; attempting to create a window with an existing label returns the existing instance rather than creating a new one.

Implementing Child Webviews

The add_child Method

For applications requiring multiple webviews within a single OS window, the Window::add_child method (lines 1052-1069 in crates/tauri/src/window/mod.rs) allows embedding additional webviews into an existing window. Child webviews share the same OS window frame but maintain independent rendering contexts.

let main_window = tauri::WindowBuilder::new(&app, "dashboard")
    .title("Dashboard")
    .inner_size(1200.0, 800.0)
    .build()?;

// Embed a child webview at specific coordinates
main_window.add_child(
    tauri::webview::WebviewBuilder::new(
        "metrics-panel",
        WebviewUrl::External("https://internal.example.com/metrics".parse().unwrap()),
    )
    .auto_resize(),
    tauri::LogicalPosition::new(0, 0),
    tauri::LogicalSize::new(600, 400),
)?;

The add_child implementation runs the builder on the main thread via a channel, ensuring thread-safe access to the window's internal state.

Handling window.open from JavaScript

The on_new_window Handler

When JavaScript executes window.open, Tauri intercepts the request through the New-Window handler. Register this handler using WebviewWindowBuilder::on_new_window (line 320 in crates/tauri/src/webview/webview_window.rs) to determine whether to spawn a new top-level window, create a child webview, or block the request.

let app_handle = app.handle().clone();

tauri::WebviewWindowBuilder::new(app, "primary", WebviewUrl::default())
    .on_new_window(move |url, features| {
        let main_win = app_handle.get_window("main").unwrap();
        
        // Convert popup to child webview
        main_win
            .add_child(
                tauri::webview::WebviewBuilder::new("popup-view", WebviewUrl::App(url)),
                features.position(),
                features.size(),
            )
            .map(|_| tauri::NewWindowResponse::Child)
    })
    .build()?;

The runtime invokes this handler inside tauri-runtime-wry (see create_window at line 2634 in crates/tauri-runtime-wry/src/lib.rs), allowing synchronous decision-making about window placement.

Threading and Runtime Considerations

Creating windows from synchronous commands can deadlock on Windows because the event loop runs on the same thread. The documentation in WebviewWindowBuilder::new (lines 60-64) explicitly warns against this pattern. Always use async commands or spawn window creation on a separate thread to prevent UI freezing.

Label uniqueness is enforced by the WindowManager; dynamic window generation requires unique identifiers such as UUIDs or incrementing counters. When a window closes, Tauri automatically destroys all attached child webviews. To persist webview state across window recreation, store the label and URL separately for later reconstruction.

Complete Multi-Window Implementation Example

This comprehensive setup demonstrates top-level windows, child webviews, and JavaScript popup handling:

use tauri::{Manager, WebviewUrl, WindowBuilder, LogicalPosition, LogicalSize};

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            // Create primary window
            let main = WindowBuilder::new(app, "main")
                .title("Main Window")
                .inner_size(1024.0, 768.0)
                .build()?;

            // Create secondary top-level window
            WindowBuilder::new(app, "tools")
                .title("Tools")
                .inner_size(400.0, 600.0)
                .build()?;

            // Add child webview to main window
            main.add_child(
                tauri::webview::WebviewBuilder::new(
                    "doc-viewer",
                    WebviewUrl::External("https://tauri.app".parse().unwrap()),
                )
                .auto_resize(),
                LogicalPosition::new(100, 100),
                LogicalSize::new(800, 600),
            )?;

            // Handle window.open requests from JavaScript
            let handle = app.handle().clone();
            tauri::WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
                .on_new_window(move |url, features| {
                    let parent = handle.get_window("main").unwrap();
                    parent
                        .add_child(
                            tauri::webview::WebviewBuilder::new("js-popup", WebviewUrl::App(url)),
                            features.position(),
                            features.size(),
                        )
                        .map(|_| tauri::NewWindowResponse::Child)
                })
                .build()?;

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("failed to run application");
}

Summary

  • Top-level windows are created via WindowBuilder::new with unique labels in crates/tauri/src/window/mod.rs, spawning independent OS windows that share the same AppHandle.
  • Child webviews use Window::add_child (line 1052) to embed multiple rendering surfaces within a single window frame.
  • JavaScript popups are intercepted via on_new_window (line 320 in webview_window.rs) and can be redirected to child webviews or new windows.
  • Thread safety requires async commands for window creation on Windows to avoid deadlocking the event loop.
  • Lifecycle management automatically destroys child webviews when their parent window closes, requiring manual state preservation if persistence is needed.

Frequently Asked Questions

How do I communicate between windows in Tauri?

Use the AppHandle to retrieve window references via app.get_window("label") and emit events using window.emit() or window.emit_to(). All windows share the same AppHandle instance, allowing cross-window messaging through Tauri's event system.

Can I create windows dynamically after the app has started?

Yes, windows can be created at any time using WindowBuilder::new or WebviewWindowBuilder::new from within Tauri commands. Ensure you use async commands or separate threads when creating windows on Windows to prevent deadlock issues documented in the WebviewWindowBuilder source.

What is the difference between a window and a webview in Tauri?

A window is an OS-level window frame managed by the WindowManager, while a webview is the rendering surface that displays HTML content. A window owns at least one webview, but can host multiple child webviews via add_child, allowing complex layouts like side-by-side panels within a single window.

Why does window creation freeze on Windows and how do I fix it?

Window creation dispatches to the event loop via RuntimeHandle::create_window. On Windows, calling this from the main thread blocks the event loop, causing a deadlock. Fix this by marking your Tauri command as async or spawning the creation logic in a separate thread, as implemented in the runtime handling at crates/tauri-runtime-wry/src/lib.rs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →