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

> Master multi-window applications in Tauri. Learn to create and manage multiple windows, share AppHandles, and coordinate lifecycles for seamless user experiences. Your complete guide.

- Repository: [Tauri/tauri](https://github.com/tauri-apps/tauri)
- Tags: how-to-guide
- Published: 2026-02-26

---

**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`](https://github.com/tauri-apps/tauri/blob/main/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:

- **WindowManager** ([`crates/tauri/src/manager/mod.rs`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri/src/manager/mod.rs)): Stores active `Window<R>` instances in a `BTreeMap` and coordinates creation, destruction, and lookup operations.
- **Window Struct** ([`crates/tauri/src/window/mod.rs`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri/src/window/mod.rs)): A thin wrapper around the runtime-specific `DetachedWindow` that provides methods like `add_child` for hosting multiple webviews.
- **WebviewWindow Builder** ([`crates/tauri/src/webview/webview_window.rs`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri/src/webview/webview_window.rs)): High-level API that constructs a `Window<R>` and immediately attaches a `Webview<R>` via `WebviewWindowBuilder`.

## 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`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri/src/window/mod.rs). Each window requires a **unique label** that serves as its identifier throughout the application lifecycle.

```rust
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`](https://github.com/tauri-apps/tauri/blob/main/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.

```rust
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`](https://github.com/tauri-apps/tauri/blob/main/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.

```rust
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`](https://github.com/tauri-apps/tauri/blob/main/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:

```rust
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`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri-runtime-wry/src/lib.rs).