$ cat /posts/tauri-20-window-management-multiple-windows-and-dialogs.md
[tags]Tauri 2.0

Tauri 2.0 Window Management Multiple Windows and Dialogs

drwxr-xr-x2026-01-285 min0 views
Tauri 2.0 Window Management Multiple Windows and Dialogs

Window management in Tauri 2.0 enables creating sophisticated multi-window desktop applications opening secondary windows for settings, previews, or tools, controlling window properties like size, position, and decorations, implementing modal dialogs for user input, coordinating communication between windows through events and shared state, and managing window lifecycle from creation to destructionโ€”essential for building professional desktop software matching native application behavior and user expectations. Tauri provides comprehensive window APIs accessible from both Rust backend and JavaScript frontend supporting programmatic window creation with custom properties, window manipulation including resize, move, minimize, maximize, and close, window state monitoring detecting focus, blur, resize, and position changes, parent-child relationships creating modal dialogs, and multi-window coordination sharing data and synchronizing state across application windows. This comprehensive guide covers understanding window architecture and WebView instances, creating new windows programmatically from Rust and frontend, configuring window properties including size, position, title, decorations, and transparency, controlling window state with minimize, maximize, fullscreen, and focus, implementing modal dialogs for user interactions, managing window communication through events and state, handling window lifecycle events properly cleaning up resources, creating parent-child window relationships, building multi-window applications with synchronized state, and implementing advanced patterns like splash screens, system tray windows, and floating panels. Mastering window patterns enables building sophisticated desktop applications with settings windows, document viewers, tool palettes, preview panels, modal dialogs, and multi-document interfaces. Before proceeding, understand event system and state management.

Window Creation Fundamentals

Windows are created using WindowBuilder in Rust or the WebviewWindow class in JavaScript providing flexible configuration options controlling appearance, behavior, and initial state.

rustcreate_windows.rs
// Rust: Creating windows from backend
use tauri::{AppHandle, Manager, WindowBuilder, WindowUrl};

#[tauri::command]
async fn open_settings_window(app: AppHandle) -> Result<(), String> {
    // Check if window already exists
    if app.get_window("settings").is_some() {
        // Focus existing window
        if let Some(window) = app.get_window("settings") {
            window.set_focus().map_err(|e| e.to_string())?;
        }
        return Ok(());
    }
    
    // Create new window
    WindowBuilder::new(
        &app,
        "settings", // Unique window label
        WindowUrl::App("settings.html".into())
    )
    .title("Settings")
    .inner_size(600.0, 400.0)
    .resizable(true)
    .center()
    .build()
    .map_err(|e| e.to_string())?;
    
    Ok(())
}

#[tauri::command]
async fn open_about_window(app: AppHandle) -> Result<(), String> {
    WindowBuilder::new(
        &app,
        "about",
        WindowUrl::App("about.html".into())
    )
    .title("About")
    .inner_size(400.0, 300.0)
    .resizable(false)
    .center()
    .decorations(true)
    .build()
    .map_err(|e| e.to_string())?;
    
    Ok(())
}

// Create window with custom properties
#[tauri::command]
async fn open_preview_window(
    app: AppHandle,
    title: String,
    width: f64,
    height: f64
) -> Result<(), String> {
    let window_label = format!("preview_{}", uuid::Uuid::new_v4());
    
    WindowBuilder::new(
        &app,
        window_label,
        WindowUrl::App("preview.html".into())
    )
    .title(&title)
    .inner_size(width, height)
    .min_inner_size(400.0, 300.0)
    .max_inner_size(1920.0, 1080.0)
    .resizable(true)
    .center()
    .build()
    .map_err(|e| e.to_string())?;
    
    Ok(())
}

// Frontend: Creating windows from JavaScript
import { WebviewWindow } from "@tauri-apps/api/window";
import { invoke } from "@tauri-apps/api/core";

// Create settings window
async function openSettings() {
  // Check if window exists
  const existing = WebviewWindow.getByLabel("settings");
  if (existing) {
    await existing.setFocus();
    return;
  }
  
  // Create new window
  const window = new WebviewWindow("settings", {
    url: "settings.html",
    title: "Settings",
    width: 600,
    height: 400,
    resizable: true,
    center: true,
  });
  
  // Listen for window creation
  window.once("tauri://created", () => {
    console.log("Settings window created");
  });
  
  window.once("tauri://error", (e) => {
    console.error("Failed to create window:", e);
  });
}

// Create modal dialog
async function openDialog() {
  const dialog = new WebviewWindow("dialog", {
    url: "dialog.html",
    title: "Confirmation",
    width: 400,
    height: 200,
    resizable: false,
    center: true,
    decorations: true,
    alwaysOnTop: true,
  });
}

// Create window with custom position
async function openToolPalette() {
  const palette = new WebviewWindow("palette", {
    url: "palette.html",
    title: "Tools",
    width: 200,
    height: 400,
    x: 100,
    y: 100,
    resizable: false,
    decorations: true,
    alwaysOnTop: true,
  });
}

Window Configuration Options

PropertyDescriptionDefaultExample
titleWindow title bar textApp name"Settings"
width, heightWindow dimensions in pixels800, 600600, 400
x, yWindow positionCenter100, 100
resizableAllow window resizingtruefalse
decorationsShow title bar and borderstruefalse
alwaysOnTopKeep window above othersfalsetrue
transparentTransparent window backgroundfalsetrue
fullscreenStart in fullscreen modefalsetrue
centerCenter window on screenfalsetrue
focusFocus window on creationtruefalse
visibleShow window immediatelytruefalse

Controlling Window State

rustwindow_control.rs
// Rust: Window control commands
use tauri::{Manager, Window};

#[tauri::command]
async fn minimize_window(window: Window) {
    window.minimize().unwrap();
}

#[tauri::command]
async fn maximize_window(window: Window) {
    window.maximize().unwrap();
}

#[tauri::command]
async fn unmaximize_window(window: Window) {
    window.unmaximize().unwrap();
}

#[tauri::command]
async fn close_window(window: Window) {
    window.close().unwrap();
}

#[tauri::command]
async fn hide_window(window: Window) {
    window.hide().unwrap();
}

#[tauri::command]
async fn show_window(window: Window) {
    window.show().unwrap();
}

#[tauri::command]
async fn set_window_title(window: Window, title: String) {
    window.set_title(&title).unwrap();
}

#[tauri::command]
async fn set_window_size(window: Window, width: f64, height: f64) {
    use tauri::Size;
    window.set_size(Size::Physical(tauri::PhysicalSize {
        width: width as u32,
        height: height as u32,
    })).unwrap();
}

#[tauri::command]
async fn set_window_position(window: Window, x: i32, y: i32) {
    use tauri::Position;
    window.set_position(Position::Physical(tauri::PhysicalPosition { x, y })).unwrap();
}

#[tauri::command]
async fn center_window(window: Window) {
    window.center().unwrap();
}

#[tauri::command]
async fn set_always_on_top(window: Window, always_on_top: bool) {
    window.set_always_on_top(always_on_top).unwrap();
}

#[tauri::command]
async fn set_fullscreen(window: Window, fullscreen: bool) {
    window.set_fullscreen(fullscreen).unwrap();
}

// Frontend: Window control
import { appWindow } from "@tauri-apps/api/window";
import { invoke } from "@tauri-apps/api/core";

// Minimize window
await appWindow.minimize();

// Maximize window
await appWindow.maximize();

// Toggle maximize
const isMaximized = await appWindow.isMaximized();
if (isMaximized) {
  await appWindow.unmaximize();
} else {
  await appWindow.maximize();
}

// Close window
await appWindow.close();

// Set title
await appWindow.setTitle("New Title");

// Resize window
await appWindow.setSize({ width: 800, height: 600 });

// Move window
await appWindow.setPosition({ x: 100, y: 100 });

// Center window
await appWindow.center();

// Toggle always on top
await appWindow.setAlwaysOnTop(true);

// Fullscreen
await appWindow.setFullscreen(true);

// Hide/Show
await appWindow.hide();
await appWindow.show();

// Get window properties
const position = await appWindow.outerPosition();
const size = await appWindow.outerSize();
const isVisible = await appWindow.isVisible();
const isFocused = await appWindow.isFocused();

console.log({ position, size, isVisible, isFocused });

Window Lifecycle Events

typescriptwindow_events.ts
// Frontend: Listening to window events
import { appWindow } from "@tauri-apps/api/window";
import { useEffect } from "react";

function WindowEventHandler() {
  useEffect(() => {
    const unlisteners: (() => void)[] = [];

    (async () => {
      // Window focus
      unlisteners.push(
        await appWindow.onFocusChanged(({ payload: focused }) => {
          console.log("Window focus:", focused);
        })
      );

      // Window resize
      unlisteners.push(
        await appWindow.onResized(({ payload: size }) => {
          console.log("Window resized:", size);
        })
      );

      // Window move
      unlisteners.push(
        await appWindow.onMoved(({ payload: position }) => {
          console.log("Window moved:", position);
        })
      );

      // Window close requested
      unlisteners.push(
        await appWindow.onCloseRequested(async (event) => {
          const confirmed = confirm("Are you sure you want to close?");
          if (!confirmed) {
            event.preventDefault();
          }
        })
      );

      // Theme changed
      unlisteners.push(
        await appWindow.onThemeChanged(({ payload: theme }) => {
          console.log("Theme changed:", theme);
        })
      );

      // File drop
      unlisteners.push(
        await appWindow.onFileDropEvent((event) => {
          if (event.payload.type === "hover") {
            console.log("Files hovering:", event.payload.paths);
          } else if (event.payload.type === "drop") {
            console.log("Files dropped:", event.payload.paths);
          } else if (event.payload.type === "cancel") {
            console.log("Drop cancelled");
          }
        })
      );
    })();

    // Cleanup
    return () => {
      unlisteners.forEach((fn) => fn && fn());
    };
  }, []);

  return null;
}

// Save window state on close
function WindowStateManager() {
  useEffect(() => {
    let unlisten: () => void;

    (async () => {
      unlisten = await appWindow.onCloseRequested(async (event) => {
        // Prevent immediate close
        event.preventDefault();

        // Save window state
        const position = await appWindow.outerPosition();
        const size = await appWindow.outerSize();
        const isMaximized = await appWindow.isMaximized();

        await invoke("save_window_state", {
          position,
          size,
          isMaximized,
        });

        // Now close
        await appWindow.close();
      });
    })();

    return () => {
      if (unlisten) unlisten();
    };
  }, []);

  return null;
}

Inter-Window Communication

Windows communicate through events and shared state enabling coordination and synchronization across multiple application windows. Review events and state management.

rustinter_window_communication.rs
// Rust: Send message to specific window
use tauri::{AppHandle, Manager};

#[tauri::command]
async fn send_to_window(
    app: AppHandle,
    target_label: String,
    message: String
) -> Result<(), String> {
    if let Some(window) = app.get_window(&target_label) {
        window.emit("message", message)
            .map_err(|e| e.to_string())?;
        Ok(())
    } else {
        Err(format!("Window '{}' not found", target_label))
    }
}

#[tauri::command]
async fn broadcast_message(app: AppHandle, message: String) {
    app.emit_all("broadcast", message).ok();
}

#[tauri::command]
async fn get_all_windows(app: AppHandle) -> Vec<String> {
    app.windows()
        .into_iter()
        .map(|(label, _)| label)
        .collect()
}

// Frontend: Sending messages between windows
import { emit, listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
import { WebviewWindow } from "@tauri-apps/api/window";

// Main window: Send message to settings window
async function sendToSettings(data: any) {
  await invoke("send_to_window", {
    target_label: "settings",
    message: JSON.stringify(data),
  });
}

// Settings window: Listen for messages
function SettingsWindow() {
  useEffect(() => {
    let unlisten: () => void;

    (async () => {
      unlisten = await listen<string>("message", (event) => {
        const data = JSON.parse(event.payload);
        console.log("Received message:", data);
        // Handle message
      });
    })();

    return () => {
      if (unlisten) unlisten();
    };
  }, []);

  return <div>Settings Window</div>;
}

// Broadcast to all windows
async function broadcastUpdate(update: any) {
  await emit("global-update", update);
}

// Listen for broadcasts
function WindowComponent() {
  useEffect(() => {
    let unlisten: () => void;

    (async () => {
      unlisten = await listen("global-update", (event) => {
        console.log("Global update:", event.payload);
      });
    })();

    return () => {
      if (unlisten) unlisten();
    };
  }, []);

  return <div>Component</div>;
}

// Request-response pattern
class WindowMessenger {
  private static pendingRequests = new Map<string, (value: any) => void>();

  static async request<T>(targetWindow: string, request: any): Promise<T> {
    const requestId = Math.random().toString(36);

    return new Promise((resolve, reject) => {
      this.pendingRequests.set(requestId, resolve);

      // Send request
      invoke("send_to_window", {
        target_label: targetWindow,
        message: JSON.stringify({
          type: "request",
          id: requestId,
          data: request,
        }),
      });

      // Timeout
      setTimeout(() => {
        if (this.pendingRequests.has(requestId)) {
          this.pendingRequests.delete(requestId);
          reject(new Error("Request timeout"));
        }
      }, 5000);
    });
  }

  static handleResponse(requestId: string, response: any) {
    const resolver = this.pendingRequests.get(requestId);
    if (resolver) {
      resolver(response);
      this.pendingRequests.delete(requestId);
    }
  }
}

// Usage
const response = await WindowMessenger.request("settings", {
  action: "get_theme",
});

Creating Modal Dialogs

typescriptmodal_dialogs.ts
// Create modal dialog with parent-child relationship
import { WebviewWindow, appWindow } from "@tauri-apps/api/window";
import { listen } from "@tauri-apps/api/event";

// Open confirmation dialog
async function openConfirmDialog(
  title: string,
  message: string
): Promise<boolean> {
  return new Promise((resolve) => {
    const dialog = new WebviewWindow("confirm-dialog", {
      url: `confirm.html?message=${encodeURIComponent(message)}`,
      title,
      width: 400,
      height: 200,
      resizable: false,
      center: true,
      alwaysOnTop: true,
      decorations: true,
    });

    // Listen for response
    listen("dialog-response", (event: any) => {
      resolve(event.payload.confirmed);
      dialog.close();
    });
  });
}

// Dialog window (confirm.html)
function ConfirmDialog() {
  const params = new URLSearchParams(window.location.search);
  const message = params.get("message") || "Are you sure?";

  const handleConfirm = async () => {
    await emit("dialog-response", { confirmed: true });
    await appWindow.close();
  };

  const handleCancel = async () => {
    await emit("dialog-response", { confirmed: false });
    await appWindow.close();
  };

  return (
    <div className="dialog">
      <p>{message}</p>
      <div className="buttons">
        <button onClick={handleConfirm}>Confirm</button>
        <button onClick={handleCancel}>Cancel</button>
      </div>
    </div>
  );
}

// Usage
const confirmed = await openConfirmDialog(
  "Delete File",
  "Are you sure you want to delete this file?"
);

if (confirmed) {
  // Proceed with deletion
}

// Input dialog
async function openInputDialog(
  title: string,
  label: string,
  defaultValue: string = ""
): Promise<string | null> {
  return new Promise((resolve) => {
    const dialog = new WebviewWindow("input-dialog", {
      url: `input.html?label=${encodeURIComponent(label)}&default=${encodeURIComponent(defaultValue)}`,
      title,
      width: 400,
      height: 150,
      resizable: false,
      center: true,
      alwaysOnTop: true,
    });

    listen("input-response", (event: any) => {
      resolve(event.payload.value);
      dialog.close();
    });

    dialog.once("tauri://destroyed", () => {
      resolve(null);
    });
  });
}

// Input dialog component
function InputDialog() {
  const params = new URLSearchParams(window.location.search);
  const label = params.get("label") || "Enter value:";
  const defaultValue = params.get("default") || "";
  const [value, setValue] = useState(defaultValue);

  const handleSubmit = async () => {
    await emit("input-response", { value });
    await appWindow.close();
  };

  const handleCancel = async () => {
    await emit("input-response", { value: null });
    await appWindow.close();
  };

  return (
    <div className="dialog">
      <label>{label}</label>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        onKeyPress={(e) => e.key === "Enter" && handleSubmit()}
        autoFocus
      />
      <div className="buttons">
        <button onClick={handleSubmit}>OK</button>
        <button onClick={handleCancel}>Cancel</button>
      </div>
    </div>
  );
}

// Usage
const name = await openInputDialog("Create Project", "Project name:", "my-project");
if (name) {
  // Create project with name
}

Window Management Best Practices

  • Unique Labels: Use unique window labels preventing conflicts and enabling window lookup
  • Check Existence: Before creating windows, check if they already exist focusing existing instances
  • Clean Up Listeners: Remove event listeners when windows close preventing memory leaks
  • Save State: Persist window size, position, and state restoring on next launch
  • Handle Close: Listen for close events saving data before window destruction
  • Center Dialogs: Use center: true for modal dialogs ensuring visibility
  • Always On Top: Set modal dialogs as always on top maintaining focus
  • Minimum Size: Set minimum window size preventing unusable layouts
  • Coordinate Windows: Use events and state for inter-window communication
  • Error Handling: Wrap window operations in try-catch handling creation failures
Performance Tip: Each window creates a new WebView instance consuming memoryโ€”limit the number of concurrent windows and close unused windows promptly. For frequently accessed windows like settings, consider showing/hiding instead of creating/destroying.

Real-World Example: Multi-Document Editor

typescriptmulti_document_editor.tsx
// Multi-document interface with window management
import { WebviewWindow } from "@tauri-apps/api/window";
import { invoke } from "@tauri-apps/api/core";
import { useState, useEffect } from "react";

interface Document {
  id: string;
  title: string;
  content: string;
  windowLabel?: string;
}

function DocumentManager() {
  const [documents, setDocuments] = useState<Document[]>([]);

  const createDocument = async () => {
    const id = Math.random().toString(36);
    const title = `Untitled ${documents.length + 1}`;

    const doc: Document = {
      id,
      title,
      content: "",
    };

    setDocuments([...documents, doc]);
    await openDocumentWindow(doc);
  };

  const openDocumentWindow = async (doc: Document) => {
    const windowLabel = `document-${doc.id}`;

    const window = new WebviewWindow(windowLabel, {
      url: `editor.html?id=${doc.id}`,
      title: doc.title,
      width: 800,
      height: 600,
      center: true,
    });

    // Update document with window label
    setDocuments((prev) =>
      prev.map((d) => (d.id === doc.id ? { ...d, windowLabel } : d))
    );

    // Listen for window close
    window.once("tauri://destroyed", () => {
      setDocuments((prev) =>
        prev.map((d) =>
          d.id === doc.id ? { ...d, windowLabel: undefined } : d
        )
      );
    });
  };

  const closeDocument = async (doc: Document) => {
    if (doc.windowLabel) {
      const window = WebviewWindow.getByLabel(doc.windowLabel);
      await window?.close();
    }

    setDocuments((prev) => prev.filter((d) => d.id !== doc.id));
  };

  const focusDocument = async (doc: Document) => {
    if (doc.windowLabel) {
      const window = WebviewWindow.getByLabel(doc.windowLabel);
      await window?.setFocus();
    } else {
      await openDocumentWindow(doc);
    }
  };

  return (
    <div className="document-manager">
      <h2>Open Documents</h2>
      <button onClick={createDocument}>New Document</button>

      <ul>
        {documents.map((doc) => (
          <li key={doc.id}>
            <span onClick={() => focusDocument(doc)}>{doc.title}</span>
            <button onClick={() => closeDocument(doc)}>Close</button>
            {doc.windowLabel && <span>๐ŸŸข</span>}
          </li>
        ))}
      </ul>
    </div>
  );
}

// Editor window
function EditorWindow() {
  const params = new URLSearchParams(window.location.search);
  const docId = params.get("id");
  const [content, setContent] = useState("");
  const [title, setTitle] = useState("Untitled");

  useEffect(() => {
    // Load document
    loadDocument(docId!);

    // Auto-save on change
    const timer = setTimeout(() => {
      saveDocument(docId!, content);
    }, 1000);

    return () => clearTimeout(timer);
  }, [content]);

  const loadDocument = async (id: string) => {
    const doc = await invoke<Document>("get_document", { id });
    setContent(doc.content);
    setTitle(doc.title);
  };

  const saveDocument = async (id: string, content: string) => {
    await invoke("save_document", { id, content });
  };

  return (
    <div className="editor">
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Document title"
      />
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="Start typing..."
      />
    </div>
  );
}

Next Steps

Conclusion

Mastering window management in Tauri 2.0 enables building sophisticated multi-window desktop applications with settings dialogs, tool palettes, preview windows, modal interactions, and multi-document interfaces matching native application behavior users expect from professional desktop software. Tauri's comprehensive window APIs provide programmatic creation with flexible configuration, state control for minimizing, maximizing, and positioning, lifecycle event handling for proper resource cleanup, inter-window communication through events and shared state, and modal dialog patterns for user interactions maintaining application flow. Understanding window patterns including unique labeling preventing conflicts, existence checking avoiding duplicates, state persistence restoring layouts, proper cleanup preventing memory leaks, and communication coordination synchronizing windows establishes foundation for professional desktop application development delivering seamless multi-window experiences. Your Tauri applications now possess powerful window management capabilities enabling features like settings panels, document viewers, tool windows, modal dialogs, and complex multi-window workflows delivering professional desktop experiences!

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.