# How to Set Up a Multi-Webview Architecture with Tauri: A Complete Guide

> Learn how to set up a multi-webview architecture in Tauri. Create a primary window and attach independent child webviews using WindowBuilder and WebviewBuilder for a shared native frame.

- Repository: [lencx/ChatGPT](https://github.com/lencx/ChatGPT)
- Tags: how-to-guide
- Published: 2026-03-06

---

**You can create a multi-webview architecture in Tauri by using `WindowBuilder` to create a primary window, then attaching multiple child webviews via `WebviewBuilder` and `add_child()`, allowing independent UI panels to share a single native window frame.**

The **lencx/ChatGPT** desktop application demonstrates a production-ready implementation of a multi-webview architecture with Tauri. This pattern enables complex layouts where distinct UI components—such as a custom title bar, side panels, and main content—operate as separate webviews within a single native window. This guide breaks down the exact implementation found in the repository, including file paths, method signatures, and runnable code examples.

## Understanding the Multi-Webview Pattern

A multi-webview architecture in Tauri consists of a **primary native window** that acts as a container for multiple **child webviews**. Each webview renders independent HTML content but shares the same OS window frame. This approach is ideal for applications requiring:

- Custom title bars that overlay native controls
- Persistent side panels or toolbars
- Isolated scripting contexts for different UI components

In the ChatGPT application, the architecture centers on a main window labeled `"core"` that hosts three distinct child webviews: `main` (external ChatGPT content), `titlebar` (custom controls), and `ask` (auxiliary input panel).

## Creating the Primary Window

The foundation of the architecture begins in [`src-tauri/src/core/setup.rs`](https://github.com/lencx/ChatGPT/blob/main/src-tauri/src/core/setup.rs), where the application constructs the primary window using `WindowBuilder`. This window serves as the parent container for all child webviews.

```rust
let mut core_window = WindowBuilder::new(&handle, "core")
    .title("ChatGPT")
    .resizable(true)
    .inner_size(800.0, 600.0)
    .min_inner_size(300.0, 200.0)
    .theme(Some(AppConf::get_theme(&handle)))
    .build()
    .expect("[core:window] Failed to build window");

```

The window is wrapped in an `Arc<Mutex<_>>` to enable safe access across asynchronous tasks. This shared state is crucial for later operations like resizing and view toggling.

## Building and Attaching Child Webviews

Once the primary window exists, the application constructs individual webviews using `WebviewBuilder` and attaches them as children using the `add_child()` method. This process occurs in the same [`setup.rs`](https://github.com/lencx/ChatGPT/blob/main/setup.rs) file immediately following window creation.

### The Three Webview Components

The ChatGPT application creates three specialized webviews:

1. **`main`** – Loads the external ChatGPT interface (`https://chatgpt.com`) and includes initialization scripts for custom functionality
2. **`titlebar`** – Renders the custom title bar UI from local [`index.html`](https://github.com/lencx/ChatGPT/blob/main/index.html)
3. **`ask`** – Displays the auxiliary "Ask" panel, also from [`index.html`](https://github.com/lencx/ChatGPT/blob/main/index.html)

```rust
// Build child webviews
let main_view = WebviewBuilder::new(
    "main",
    WebviewUrl::App("https://chatgpt.com".into())
).auto_resize()
 .initialization_script(&AppConf::load_script(&handle, "ask.js"))
 .initialization_script(INIT_SCRIPT);

let titlebar_view = WebviewBuilder::new(
    "titlebar",
    WebviewUrl::App("index.html".into())
).auto_resize();

let ask_view = WebviewBuilder::new(
    "ask",
    WebviewUrl::App("index.html".into())
).auto_resize();

```

### Platform-Specific Layout Handling

The attachment process differs between macOS and other platforms to accommodate platform-specific title bar behaviors. The `add_child()` method positions each webview within the parent window's coordinate space:

```rust
// Non-macOS attachment example
win.add_child(ask_view,
    LogicalPosition::new(0.0, (win_size.height as f64 / scale_factor) - ask_mode_height),
    PhysicalSize::new(win_size.width, ask_height))
   .unwrap();

win.add_child(titlebar_view,
    LogicalPosition::new(0.0,
        (win_size.height as f64 / scale_factor) - ask_mode_height - TITLEBAR_HEIGHT),
    PhysicalSize::new(win_size.width, titlebar_height))
   .unwrap();

win.add_child(main_view,
    LogicalPosition::new(0.0, 0.0),
    PhysicalSize::new(
        win_size.width,
        win_size.height - (ask_height + titlebar_height)))
   .unwrap();

```

Layout constants such as `ASK_HEIGHT` and `TITLEBAR_HEIGHT` are defined in [`src-tauri/src/core/constant.rs`](https://github.com/lencx/ChatGPT/blob/main/src-tauri/src/core/constant.rs), centralizing configuration for easy adjustment.

## Handling Dynamic Resizing

When the user resizes the main window, the application must recalculate the positions and dimensions of all child webviews. This logic resides in the `on_window_event` callback within [`setup.rs`](https://github.com/lencx/ChatGPT/blob/main/setup.rs):

```rust
win.on_window_event(move |event| {
    if let WindowEvent::Resized(size) = event {
        let win = window_clone.lock().unwrap();

        let main_view = win.get_webview("main")
            .expect("[view:main] Failed to get webview window");
        let titlebar_view = win.get_webview("titlebar")
            .expect("[view:titlebar] Failed to get webview window");
        let ask_view = win.get_webview("ask")
            .expect("[view:ask] Failed to get webview window");

        // Re-apply positions & sizes based on new dimensions
        set_view_properties(&main_view,
            LogicalPosition::new(0.0, 0.0),
            PhysicalSize::new(size.width, size.height - (ask_height + titlebar_height)));
        // Similar updates applied to titlebar_view and ask_view...
    }
});

```

The `get_webview` method retrieves child views by their string labels (`"main"`, `"titlebar"`, `"ask"`), enabling targeted manipulation of specific UI components.

## Managing Secondary Windows

Beyond the multi-webview primary window, the application supports spawning entirely separate native windows. The `open_settings` command in [`src-tauri/src/core/window.rs`](https://github.com/lencx/ChatGPT/blob/main/src-tauri/src/core/window.rs) demonstrates this pattern:

```rust
#[command]
pub fn open_settings(app: AppHandle) {
    match app.get_webview_window(WINDOW_SETTINGS) {
        Some(window) => { window.show().unwrap(); }
        None => {
            WebviewWindowBuilder::new(
                &app,
                WINDOW_SETTINGS,
                WebviewUrl::App("index.html".into())
            ).build().unwrap();
        }
    }
}

```

This command checks for an existing window labeled `WINDOW_SETTINGS` (defined in [`constant.rs`](https://github.com/lencx/ChatGPT/blob/main/constant.rs)) before creating a new one, preventing duplicate windows while allowing the multi-webview architecture to coexist with traditional multi-window patterns.

## Summary

- **Primary window container**: Use `WindowBuilder` to create the native window that hosts all child webviews, storing it in `Arc<Mutex<_>>` for thread-safe access.
- **Child webview creation**: Build individual views with `WebviewBuilder`, assigning unique labels like `"main"`, `"titlebar"`, and `"ask"` for later reference.
- **Attachment strategy**: Call `add_child()` on the parent window to register webviews with specific `LogicalPosition` and `PhysicalSize` values, adjusting for platform differences between macOS and other systems.
- **Dynamic layout**: Implement `on_window_event` handlers to recalculate child view geometries when the window resizes, using `get_webview()` to retrieve specific child instances.
- **Configuration management**: Centralize layout constants like `ASK_HEIGHT` and `TITLEBAR_HEIGHT` in a dedicated constants file for maintainable UI adjustments.

## Frequently Asked Questions

### How do you access individual webviews after creating them?

Use the `get_webview` method on the parent window object, passing the string label assigned during `WebviewBuilder` creation. For example, `win.get_webview("main")` retrieves the main content view, allowing you to call methods like `set_position` or `set_size` on that specific instance.

### Can child webviews load external URLs instead of local files?

Yes. The `WebviewBuilder` accepts a `WebviewUrl` enum that supports both local app paths and external URLs. In the ChatGPT application, the `main` view loads `https://chatgpt.com` while the `titlebar` and `ask` views load local [`index.html`](https://github.com/lencx/ChatGPT/blob/main/index.html) files, demonstrating mixed content sources within the same multi-webview architecture.

### How do you prevent layout issues when the user resizes the window?

Implement an `on_window_event` callback that listens for `WindowEvent::Resized` events. Inside this handler, recalculate the `LogicalPosition` and `PhysicalSize` for each child webview based on the new window dimensions, then apply these values using `set_position` and `set_size`. Store layout constants centrally to ensure consistent calculations across resize events.

### Is it possible to toggle visibility of specific webviews dynamically?

Yes. You can toggle auxiliary views by updating your application state and then repositioning or resizing the target webview. The ChatGPT application uses a `set_view_ask` command that updates the configuration and then retrieves the `ask` webview via `get_webview` to adjust its position, effectively showing or hiding the panel within the shared window space.