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

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, where the application constructs the primary window using WindowBuilder. This window serves as the parent container for all child webviews.

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 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
  3. ask – Displays the auxiliary "Ask" panel, also from index.html
// 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:

// 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, 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:

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 demonstrates this pattern:

#[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) 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 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.

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 →