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:
main– Loads the external ChatGPT interface (https://chatgpt.com) and includes initialization scripts for custom functionalitytitlebar– Renders the custom title bar UI from localindex.htmlask– Displays the auxiliary "Ask" panel, also fromindex.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
WindowBuilderto create the native window that hosts all child webviews, storing it inArc<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 specificLogicalPositionandPhysicalSizevalues, adjusting for platform differences between macOS and other systems. - Dynamic layout: Implement
on_window_eventhandlers to recalculate child view geometries when the window resizes, usingget_webview()to retrieve specific child instances. - Configuration management: Centralize layout constants like
ASK_HEIGHTandTITLEBAR_HEIGHTin 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →