tauri-windows
Use when creating, customizing, or managing Tauri v2 windows — decorations, transparency, custom titlebar, multi-window, child webviews, splashscreen pattern, or mobile multi-window setups.
Tauri v2 Window Management
Tauri windows are webview windows — each one is a native OS window hosting a webview that loads a
URL (local asset or remote). They can be declared statically in tauri.conf.json or built at
runtime from JS or Rust.
1. Static config — tauri.conf.json
Declare every window your app boots with under app.windows[]. Each entry needs a unique label
(used to address it later) and a url (path to a frontend asset, or remote URL).
{
"app": {
"windows": [
{
"label": "main",
"url": "index.html",
"title": "My App",
"width": 1024,
"height": 720,
"minWidth": 480,
"minHeight": 360,
"decorations": true,
"transparent": false,
"alwaysOnTop": false,
"resizable": true,
"fullscreen": false,
"center": true,
"visible": true
}
]
}
}
Common keys: label, url, title, width/height,
minWidth/minHeight/maxWidth/maxHeight, x/y, center, resizable, maximized,
fullscreen, decorations, transparent, alwaysOnTop, alwaysOnBottom, skipTaskbar,
visible, focus, acceptFirstMouse, tabbingIdentifier (macOS), titleBarStyle (macOS),
hiddenTitle (macOS).
See templates/tauri.conf.windows.json for a splash + main two-window setup.
2. Custom titlebar (frameless window)
Drop the native chrome with decorations: false, then render your own bar in HTML.
Required capability — add to src-tauri/capabilities/default.json:
{
"permissions": [
"core:window:default",
"core:window:allow-start-dragging"
]
}
HTML — any element with data-tauri-drag-region becomes a drag handle:
<div class="titlebar">
<div class="titlebar-drag" data-tauri-drag-region></div>
<div class="titlebar-controls">
<button id="titlebar-minimize" aria-label="Minimize">—</button>
<button id="titlebar-maximize" aria-label="Maximize">▢</button>
<button id="titlebar-close" aria-label="Close">✕</button>
</div>
</div>
JS — wire buttons to getCurrentWindow():
import { getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
document.getElementById('titlebar-minimize')?.addEventListener('click', () => win.minimize());
document.getElementById('titlebar-maximize')?.addEventListener('click', () => win.toggleMaximize());
document.getElementById('titlebar-close')?.addEventListener('click', () => win.close());
If you need finer control than data-tauri-drag-region (e.g. double-click-to-maximize on a
non-drag-region row):
document.getElementById('titlebar')?.addEventListener('mousedown', (e) => {
if (e.buttons !== 1) return;
e.detail === 2 ? win.toggleMaximize() : win.startDragging();
});
Full snippet in templates/custom-titlebar.html.
3. Runtime window creation
JavaScript
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
const settings = new WebviewWindow('settings', {
url: 'settings.html',
title: 'Settings',
width: 640,
height: 480,
resizable: false,
center: true,
});
settings.once('tauri://created', () => console.log('settings window ready'));
settings.once('tauri://error', (e) => console.error('failed to create window', e));
Labels are unique identifiers — creating a second window with the same label fails. To reuse one, look it up first:
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
const existing = await WebviewWindow.getByLabel('settings');
if (existing) await existing.setFocus();
else new WebviewWindow('settings', { url: 'settings.html' });
Rust
use tauri::{WebviewUrl, WebviewWindowBuilder};
tauri::Builder::default()
.setup(|app| {
WebviewWindowBuilder::new(app, "settings", WebviewUrl::App("settings.html".into()))
.title("Settings")
.inner_size(640.0, 480.0)
.resizable(false)
.center()
.build()?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
4. Window methods and events
import { getCurrentWindow, LogicalSize, LogicalPosition } from '@tauri-apps/api/window';
const win = getCurrentWindow();
await win.show();
await win.hide();
await win.close();
await win.center();
await win.setFocus();
await win.setSize(new LogicalSize(800, 600));
await win.setPosition(new LogicalPosition(100, 100));
await win.setAlwaysOnTop(true);
await win.setTitle('New title');
const unlistenClose = await win.listen('tauri://close-requested', async (e) => {
// call e.preventDefault() equivalent: just don't call win.close()
// run cleanup, then close manually
await win.destroy();
});
await win.listen('tauri://focus', () => console.log('focused'));
await win.listen('tauri://blur', () => console.log('blurred'));
Other useful events: tauri://resize, tauri://move, tauri://scale-change,
tauri://theme-changed, tauri://file-drop.
5. Transparency and vibrancy
Set transparent: true in the window config, then apply a translucent CSS background. For native
blur/vibrancy effects (macOS NSVisualEffect, Windows mica/acrylic), use the
window-vibrancy crate from your Rust setup hook:
use tauri::Manager;
use window_vibrancy::{apply_vibrancy, apply_mica, NSVisualEffectMaterial};
tauri::Builder::default()
.setup(|app| {
let win = app.get_webview_window("main").unwrap();
#[cfg(target_os = "macos")]
apply_vibrancy(&win, NSVisualEffectMaterial::HudWindow, None, None)
.expect("vibrancy unsupported");
#[cfg(target_os = "windows")]
apply_mica(&win, None).expect("mica unsupported");
Ok(())
})
On macOS you can also use the native transparent titlebar without window-vibrancy:
use tauri::{TitleBarStyle, WebviewUrl, WebviewWindowBuilder};
WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.title_bar_style(TitleBarStyle::Transparent)
.inner_size(800.0, 600.0)
.build()?;
6. Splashscreen pattern
The idea: ship a tiny splash window that’s visible at boot, keep main hidden, do init work in
the Rust setup hook, then close splash and show main.
Config (see templates/tauri.conf.windows.json):
{
"windows": [
{ "label": "splash", "url": "splash.html", "width": 400, "height": 200, "decorations": false, "center": true },
{ "label": "main", "url": "index.html", "width": 1024, "height": 720, "visible": false }
]
}
Rust — close splash, show main once setup is done:
use tauri::Manager;
tauri::Builder::default()
.setup(|app| {
let handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
// heavy init: open DB, warm caches, fetch session, etc.
tokio::time::sleep(std::time::Duration::from_secs(2)).await; // demo only
if let Some(splash) = handle.get_webview_window("splash") { let _ = splash.close(); }
if let Some(main) = handle.get_webview_window("main") { let _ = main.show(); }
});
Ok(())
})
Use tokio::time::sleep, not std::thread::sleep — the latter blocks the async runtime. If the
frontend also has init work, have it invoke() a frontend_ready command and only close splash
when both backend and frontend have signaled completion (typically via a Mutex<SetupState> shared
via app.manage(...)).
7. Multi-window communication
Labels are how you address other windows. Emit to a specific label:
import { emitTo } from '@tauri-apps/api/event';
await emitTo('settings', 'theme-changed', { theme: 'dark' });
use tauri::Emitter;
app.emit_to("settings", "theme-changed", "dark")?;
Listen in the target window:
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen<{ theme: string }>('theme-changed', (e) => {
applyTheme(e.payload.theme);
});
Plain emit() broadcasts to all windows; prefer emitTo(label, ...) when only one window cares.
8. Window menus (desktop)
Attach a native menu bar from Rust with MenuBuilder:
use tauri::menu::{MenuBuilder, SubmenuBuilder, PredefinedMenuItem};
tauri::Builder::default()
.setup(|app| {
let file = SubmenuBuilder::new(app, "File")
.text("new", "New Window")
.separator()
.item(&PredefinedMenuItem::quit(app, None)?)
.build()?;
let menu = MenuBuilder::new(app).item(&file).build()?;
app.set_menu(menu)?;
Ok(())
})
.on_menu_event(|app, event| match event.id().as_ref() {
"new" => { /* open a new window */ }
_ => {}
})
macOS requires every top-level item to be a Submenu; bare items at the top level are ignored. The
JS Menu.new() / Submenu.new() APIs mirror the Rust builders.
9. Mobile multi-window caveats
Multi-window on mobile requires Android 12L (API 32)+ or iOS 13+. Probe
app.supportsMultipleWindows at runtime before opening one.
- Android phones: new windows are pushed onto the activity back stack — Back returns to the previous activity rather than splitting the view. Activity Embedding (true side-by-side) only works on tablets and foldables.
- iPhone: opening a second window typically replaces the current scene rather than showing both. Concurrent windows are practical only on iPad (Stage Manager / multi-scene).
- Prefer browser-history routing over hash routing so each scene can hold its own path.
Most desktop-style multi-window UX should degrade to a single-window tabbed UI on phones.
Templates
templates/tauri.conf.windows.json— two-window splash + main config.templates/custom-titlebar.html— frameless titlebar HTML + CSS + JS.