Creating Windows

Define and spawn application windows in Tauri v2 using the configuration file, Rust backend, and JavaScript frontend

A Tauri application is built from one or more native operating system windows. Each window hosts a webview that renders your React frontend. Understanding how windows are defined and created is the first step in configuring how your app appears and behaves.

Defining Windows in the Configuration File

The simplest way to add windows to a Tauri app is to list them in the tauri.conf.json file under app.windows. When the application starts, Tauri reads this array and creates each window automatically — before your Rust or React code runs.

Every window definition needs a few core pieces of information: a unique label to identify it, a title for the titlebar, and usually a URL pointing to the frontend page it should load. The minimal configuration looks like this:

src-tauri/tauri.conf.json
{
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "My Tauri App",
        "url": "index.html"
      }
    ]
  }
}

If you do not provide a windows array at all, Tauri creates a single window with the label "main" and sensible defaults. Adding your own array gives you control.

Default window label:

Tauri expects at least one window to exist, and by convention the primary window uses the label "main". Many APIs will look for a window with that label when you call methods like getCurrentWindow() without arguments.

For an app that needs multiple windows from the moment it launches, you can declare them all in the configuration file. Each entry is an independent window:

src-tauri/tauri.conf.json
{
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "Dashboard",
        "url": "index.html",
        "width": 1024,
        "height": 768
      },
      {
        "label": "settings",
        "title": "Settings",
        "url": "settings.html",
        "width": 600,
        "height": 400
      }
    ]
  }
}

Tauri will open both windows when the app starts. Each window loads its own frontend entry point: the main window loads the default index.html, and the settings window loads settings.html, which you would need to create as a separate HTML file served by Vite (or mapped through a router). In practice, for React apps, you often serve the same index.html and let client‑side routing decide what to show based on the window label. We’ll return to that idea in the section on window URLs.

Creating Windows Programmatically

Static configuration covers windows you know about ahead of time. Many real applications need to open new windows dynamically — a detail pane, a help window, or a secondary view that the user can spawn on demand. Tauri allows you to create windows at runtime from either the Rust backend or the JavaScript frontend. Both approaches produce the same kind of native window; the choice depends on where your creation logic belongs.

In Rust, you use the WebviewWindowBuilder to construct a new window and then call .build() to spawn it. This is typically done inside the setup closure or inside a Tauri command so that it runs in response to a frontend invoke().

src-tauri/src/lib.rs
use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            // Dynamically create a secondary window on startup
            tauri::WebviewWindowBuilder::new(
                app,
                "help",                            // unique label
                tauri::WebviewUrl::App("help.html".into())
            )
            .title("Help & Documentation")
            .inner_size(800.0, 600.0)
            .build()?;
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The WebviewUrl::App variant points to a file inside your frontend’s distDir. You could also use WebviewUrl::External("https://docs.myapp.com") to load a remote URL. The builder chain lets you set size, position, decorations, and many other options before the window appears.

Duplicate labels will crash the app:

If a window with the label "help" already exists (either defined in tauri.conf.json or created earlier), calling .build() will panic. Always check with app.get_webview_window("help") before creating a window with the same label.

For the JavaScript approach to work, you need to grant your main window the permission to create other windows. Add the following to your capability file:

src-tauri/capabilities/default.json
{
  "identifier": "main-capability",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "core:window:allow-create"
  ]
}

"core:default" bundles several sensible permissions. The explicit "core:window:allow-create" is what unlocks new WebviewWindow().

Window Labels — the Permanent Identifier

Every window you create — whether in config, Rust, or JavaScript — must have a unique string label. This label stays with the window for its entire lifetime and is the primary way you reference it later.

Think of the label as the window’s name. You use it to:

  • Close a specific window: app.get_webview_window("help").unwrap().close() in Rust, or new WebviewWindow('help').close() in JS after retrieving it.
  • Send events to a particular window: window.emit_to("help", "event-name", payload).
  • Check if a window already exists before creating a duplicate: app.get_webview_window("help").is_some().

Labels are case‑sensitive and should be short, descriptive, and kebab‑case if you need multiple words (e.g., "settings-panel"). The label "main" is special only by convention — Tauri itself does not treat it differently, but many plugins and example code assume a window with that label exists.

A common beginner mistake is to reuse a label when creating a second window of the same type. For example, opening a “detail” window for every clicked item and always giving it the label "detail". The second attempt will fail because the window already exists. If you need multiple instances, generate a unique label for each one, such as "detail-{itemId}".

Duplicate label panics in Rust:

In Rust, calling .build() on a WebviewWindowBuilder with a label that already belongs to an existing window causes an immediate panic. Always guard with app.get_webview_window(label) first, or use .build() in contexts where you are certain the label is fresh.

Window Titles — What the User Sees

The title is the human‑readable text shown in the window’s titlebar. You can set it at creation time and change it later.

In the configuration file, the title field is a static string. At runtime, both Rust and JavaScript provide a set_title method:

src-tauri/src/lib.rs
// In a command or setup
if let Some(window) = app.get_webview_window("main") {
    window.set_title("Dashboard — Project X").unwrap();
}
src/App.tsx
import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
appWindow.setTitle('Dashboard — Project X');

Changing the title dynamically is useful for editors showing a filename, dashboards showing a user’s name, or any view where the title should reflect the current content.

Window URLs — What the Window Actually Loads

Every Tauri window needs a URL to fetch its content. In development, the URL points to your Vite dev server (e.g., http://localhost:1420). In production, it points to a file inside the bundled application. Tauri handles this translation automatically based on the build.devUrl and build.frontendDist fields in the build configuration, so you rarely need to think about it.

When you set "url": "index.html" in tauri.conf.json, you are telling Tauri: “In development, load http://localhost:1420/index.html; in production, load the index.html file from the final bundle.” This is the default for the main window.

You can override the URL to load a completely different page — even an external website:

src-tauri/tauri.conf.json
{
  "app": {
    "windows": [
      {
        "label": "docs",
        "title": "Documentation",
        "url": "https://docs.myapp.com"
      }
    ]
  }
}

For React apps, you often want multiple windows to share the same SPA entry point but show different routes. You can achieve this by keeping the URL as index.html and passing a custom query parameter or using the window label to decide the initial route inside your React code:

src-tauri/tauri.conf.json
{
  "app": {
    "windows": [
      {
        "label": "settings",
        "title": "Settings",
        "url": "index.html?view=settings"
      }
    ]
  }
}

Then, in your App.tsx, read the query parameter on startup and navigate accordingly. Alternatively, you can use the WebviewUrl::App variant in Rust to append a hash or query string dynamically.

External URLs need the correct permissions:

Loading an external domain (like https://docs.myapp.com) requires the URL to be allowed by your Content Security Policy. Tauri’s default CSP blocks external scripts and connections unless you explicitly configure them in tauri.conf.json under app.security.csp.

Working with Multiple Windows

Once you move beyond a single window, a few practical patterns become important.

Checking for existing windows. Before creating a window, verify that the label is not already taken:

src/App.tsx
import { getCurrentWindow } from '@tauri-apps/api/window';
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
async function openOrFocusHelp() {
  const existing = await WebviewWindow.getByLabel('help');
  if (existing) {
    existing.setFocus();
  } else {
    new WebviewWindow('help', { url: 'help.html', title: 'Help' });
  }
}

Closing windows. Any window can close itself or another window (if it has the appropriate permission). The core:window:allow-close permission lets a window call .close() on any window, not just itself.

Communicating between windows. Use Tauri’s event system. Each window can emit events and listen for events on other windows. This is covered in a later section on window communication, but the mental model is simple: you emit to a specific window label, and that window’s listen callback fires.

Window lifecycle. A window emits events for creation, focus, blur, minimize, maximize, and close. You can listen for these events from the frontend to synchronize state across windows.

Correctly managing multiple windows:

If you open a secondary window from JavaScript and then close it with helpWindow.close(), and your main window correctly handles the tauri://closed event to update UI state (like toggling a button), you have set up window management correctly. Test this flow early — it’s the most common source of subtle bugs in multi‑window apps.

Common Mistakes When Creating Windows

  • Reusing a window label. As emphasized, a label must be unique across the entire application. Trying to create a second window with the same label crashes the Rust process.
  • Missing the core:window:allow-create permission. The frontend will silently fail to open a window, and the error may not surface in the UI unless you explicitly listen for the tauri://error event.
  • Assuming the main window always exists. If you remove the "main" entry from tauri.conf.json and rely entirely on dynamic creation, make sure some code creates the initial window — otherwise your app will start with no visible UI.
  • Forgetting to handle the tauri://created event for important logic. The new WebviewWindow() call returns an object immediately, but the underlying webview might take a moment to load. If you need to send data to the new window right away, wait for the tauri://created event to fire before sending messages.

Building a mental model around window creation in Tauri is about understanding the three entry points — config, Rust, JavaScript — and knowing when to use each. The configuration file is for windows that are part of the app’s permanent structure. Rust gives you full control over creation timing and pre‑creation logic (like checking saved state). JavaScript puts window creation directly in the hands of the user’s interactions. All three produce the same native window, and once you grasp the label‑driven lifecycle, you can mix them freely.