How to Manage Multiple WebViews as Children of a Parent Window in Tauri

In Tauri, you can host multiple WebViews within a single native window by assigning each child a unique label, attaching them to a shared parent via WebviewBuilder, and routing the correct React component based on the active webview label.

The lencx/ChatGPT repository demonstrates a production-ready architecture for managing multiple webviews as children of a parent window in Tauri. This pattern allows the main application window to host isolated UI components—such as a custom titlebar, chat input, and settings panel—each running in its own WebView instance while sharing the same native window container.

Creating Child WebViews in Rust

To manage multiple webviews as children of a parent window in Tauri, you define the parent window first, then spawn child WebviewWindow instances attached to it. In src-tauri/src/core/setup.rs, the setup logic creates a main window and attaches three labeled children: "titlebar", "ask", and "settings".

Defining the Parent-Child Relationship

Use WebviewBuilder::new() with the .parent() method to attach a child to an existing window. Each child requires a unique label string that identifies it across the application lifecycle.

// src-tauri/src/core/setup.rs
let main_window = app.get_webview_window("main").unwrap();

let titlebar = WebviewBuilder::new(&app, "titlebar", "src/view/Titlebar.html")
    .title("ChatGPT – Titlebar")
    .parent(&main_window)          // attach to parent
    .focused(false)               // start unfocused
    .build()
    .expect("[view:titlebar] Failed to get webview window");

Repeat this pattern for each additional child webview, ensuring every label is unique.

Retrieving Child Windows by Label

The get_webview helper in src-tauri/src/core/window.rs allows other Rust modules to fetch a specific child window programmatically using its label string. This enables dynamic focus changes, visibility toggles, or event emissions to specific children without affecting the parent or sibling webviews.

Routing JavaScript Components by WebView Label

On the frontend, each child webview loads the same React entry point but renders a different component based on its assigned label. The src/App.tsx file uses getCurrentWebview() from @tauri-apps/api/webview to determine which UI to mount.

// src/App.tsx
import { getCurrentWebview } from '@tauri-apps/api/webview';
import Titlebar from '~view/Titlebar';
import Ask from '~view/Ask';
import Settings from '~view/Settings';

const viewMap = {
  titlebar: <Titlebar />,
  ask: <Ask />,
  settings: <Settings />,
};

export default function App() {
  const { label } = getCurrentWebview();
  return viewMap[label as keyof typeof viewMap] ?? null;
}

This label-based routing ensures that the "titlebar" webview renders only the Titlebar component, while the "ask" webview renders the chat input interface, even though both share the same parent window.

Communicating Between Parent and Child WebViews

Child webviews communicate with the parent and each other through Tauri's event system. The parent can emit events to a specific child by label, and children can listen for those events using the @tauri-apps/api/event module.

Emitting Events from Rust to Children

In src-tauri/src/core/constant.rs, the application broadcasts navigation changes to the "titlebar" child by evaluating JavaScript that targets the specific webview label:

// src-tauri/src/core/constant.rs
pub fn emit_navigation_change(window: &Window, url: &str) {
    window
        .eval(&format!(
            "window.__TAURI__.webviewWindow.WebviewWindow.getByLabel('titlebar')
                .emit('navigation:change', {{ url: '{}' }});",
            url
        ))
        .unwrap();
}

Listening for Events in React

The child component listens for the emitted event using the listen function. In src/view/Titlebar.tsx, the titlebar subscribes to navigation:change events to update its UI when the parent detects a URL change.

// src/view/Titlebar.tsx
import { listen } from '@tauri-apps/api/event';
import { useEffect } from 'react';

export default function Titlebar() {
  useEffect(() => {
    const unlisten = listen('navigation:change', (event) => {
      console.log('New URL:', event.payload.url);
    });
    return () => {
      (async () => (await unlisten)())();
    };
  }, []);
  // …render UI
}

Lifecycle Management for Child WebViews

Managing the lifecycle of child webviews involves creation, focus control, and destruction. In the lencx/ChatGPT implementation:

  • Creation occurs once during application startup in src-tauri/src/core/setup.rs
  • Focus can be shifted programmatically using Webview.getByLabel('ask')?.setFocus() as shown in src-tauri/src/core/cmd.rs
  • Destruction calls the close() method on the WebviewWindow instance when the component needs to be removed

This architecture maintains a clean separation of concerns: the Rust backend owns the window hierarchy and lifecycle, while the React frontend handles presentation logic based on the active webview label.

Summary

  • Unique labels identify each child webview within a parent window, enabling targeted control and routing.
  • WebviewBuilder::new().parent(&main_window) attaches children to a shared native window in src-tauri/src/core/setup.rs.
  • getCurrentWebview() allows the React frontend in src/App.tsx to render the correct component for each child.
  • Event emitters and listeners bridge the gap between parent Rust code and child JavaScript contexts.
  • Programmatic focus and close methods in src-tauri/src/core/cmd.rs manage child webview states without reloading the parent.

Frequently Asked Questions

How do you create multiple webviews in a single Tauri window?

Use WebviewBuilder::new() in your Rust setup code to create each child, passing the parent window reference to the .parent() method. Assign a unique label to each webview (e.g., "titlebar", "ask") so you can reference them later for events or focus management.

How does the frontend know which webview it is rendering?

The frontend calls getCurrentWebview() from @tauri-apps/api/webview, which returns an object containing the label string assigned during creation. The React application maps this label to a specific component dictionary to render the correct UI for that child.

How do child webviews communicate with the parent window?

Communication flows through Tauri's event system. The parent emits events to a specific child using the webview label and window.eval(), while children listen using the listen function from @tauri-apps/api/event. This pattern allows bi-directional messaging without direct DOM access between webviews.

Can you dynamically create or destroy child webviews at runtime?

Yes, though the lencx/ChatGPT repository creates children at startup for performance. You can instantiate new WebviewWindow instances dynamically using the same WebviewBuilder pattern, and close them by calling .close() on the window handle retrieved via get_webview(label) or WebviewWindow.getByLabel().

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 →