Creating Windows in Tauri

Creating a Window with the WebviewWindowBuilder

To make new webviews, we can use the tauri::WebviewWindowBuilder struct, which uses the builder pattern.

Here's the most basic way to build a window:

#[tauri::command]
fn new_window(app: AppHandle) {
WebviewWindowBuilder::new(&app, "window-1", WebviewUrl::App("index.html".into()))
.build()
.unwrap();
}

This command takes an AppHandle as an argument, which is required for the WebviewWindowBuilder's new function. It also needs a unique label, "window-1", and a WebViewUrl enum. Here, we use the app handle to set up the webview and point it to the index.html file.

The WebviewWindowBuilder takes three arguments:

  • A reference to tauri::AppHandle.
  • A unique label. If it's not unique, the build() function will return an Err.
  • The final argument is a WebviewUrl enum. Most of the time, WebviewUrl::App is what will be used, but you can create a webview to an external URL or a custom protocol.

If we leave the command like this, our application will crash if this command is called twice.


Creating Unlimited New Windows

To create multiple windows of the same webview, all you need to do is create a unique label for each. To do this, we'll get the count of the current webviews and include that in our label.

#[tauri::command]
fn new_window(app: AppHandle) -> tauri::Result<()> {
let len = app.webview_windows().len();
 
WebviewWindowBuilder::new(
&app,
format!("window-{}", len),
WebviewUrl::App("index.html".into()),
)
.build()?;
 
Ok(())
}

This example returns a tauri::Result, and we use the ? operator to return an error instead of panicking.


Focusing a Window

If we want to only have a single instance of a particular window, we can do a simple check to see if the window exists. If it does, we can bring it to the front and focus it.

#[tauri::command]
fn new_window_or_focus(app: AppHandle) -> tauri::Result<()> {
match app.webview_windows().get("focus") {
None => {
WebviewWindowBuilder::new(&app, "focus", WebviewUrl::App("index.html".into()))
.build()?;
}
Some(window) => {
window.set_focus()?;
}
}
 
Ok(())
}

Here we get the webviews as a HashMap from app.webview_windows(). Then we try to get the window labeled "focus". If it's None, we'll build the window. Otherwise, we call set_focus() on the window instance.


Applying Effects

We can also apply effects to our windows while building them.

First, for macOS, we need to allow the macos private api in tauri.conf.json, and we need to add the macos-private-api flag to our Cargo.toml:

tauri.conf.json

{
"app": {
...
"macOSPrivateApi": true
}
}

Cargo.toml

tauri = { version = "2", features = ["macos-private-api"] }
#[tauri::command]
fn effects(app: AppHandle) -> tauri::Result<()> {
WebviewWindowBuilder::new(&app, "effects", WebviewUrl::App("effects.html".into()))
.title("Transparent Effects")
.resizable(false)
.theme(Some(tauri::Theme::Dark)) // Changes theme on other windows
.closable(false)
.transparent(true)
.inner_size(400.0, 800.0)
.effects(WindowEffectsConfig {
effects: vec![
// For macOS
WindowEffect::HudWindow,
// For Windows
WindowEffect::Acrylic,
],
state: None,
radius: Some(24.0),
color: None,
})
.build()?;
 
Ok(())
}

Here we customize the window quite a bit:

  • We start by calling new, but here we use effects.html instead of index as a different entry point.
  • First, we set a custom title for the window.
  • We disable the ability to resize the window.
  • We set the theme to dark mode (this will affect media queries in CSS).
  • We disable the close button on the window.
  • We set the window size to 400px wide and 800px tall.
  • We set the window effects:
    • We use the HudWindow effect for macOS.
    • And the Acrylic effect for Windows.
    • We set the window radius (this only affects macOS).

In effects.html, we set the body to transparent and the font to white for dark mode:

<body style="background: transparent; color: white; font-family: system-ui;">
<h1>Window Effects</h1>
<p>
The body of the HTML is transparent and has the
<code>WindowEffect::HudWindow</code> effect applied.
</p>
</body>

Creating Floating Windows

We can also create windows that will always be on top of all other windows on a user's desktop, and we can remove decorations.

#[tauri::command]
fn floating(app: AppHandle) -> tauri::Result<()> {
match app.webview_windows().get("floating") {
None => {
WebviewWindowBuilder::new(&app, "floating", WebviewUrl::App("index.html".into()))
.always_on_top(true)
.decorations(false)
.inner_size(400.0, 400.0)
.position(0.0, 0.0)
.build()?;
}
Some(window) => {
window.close()?;
}
}
Ok(())
}

This example will:

  • Create a window or close it if it exists.
  • Make the window always remain on top.
  • Remove decorations (the top bar including title, close, and minimize/maximize buttons).
  • Set the size and set the window position to the top-left of the screen.

Now we have a window that can be toggled to always float at the top-left of the screen. However, if we'd like more control over where the window appears, we'll need to use a Tauri plugin.


Tray Icons and Positioning

First, let's add the positioner plugin:

cargo tauri add positioner

Next, add the tray-icon feature flag to Tauri and the plugin-positioner crates:

tauri = { version = "2", features = ["macos-private-api", "tray-icon"] }
tauri-plugin-positioner = { version = "2", features = ["tray-icon"] }

Now we can dive into the Rust code. First, we customize our app using the setup() hook in the run function:

pub fn run() {
tauri::Builder::default()
// Other plugins and invoke handler, etc.
// New registered plugin
.plugin(tauri_plugin_positioner::init())
// New setup hook
.setup(|app| {
// Here we build the tray icon and give it an ID.
tauri::tray::TrayIconBuilder::with_id("main")
.icon(app.default_window_icon().unwrap().clone())
.menu_on_left_click(false)
.on_tray_icon_event(|tray_handle, event| {
// we pass the tray event to the positioner plugin so it can register the
// location of the tray icon.
tauri_plugin_positioner::on_tray_event(tray_handle.app_handle(), &event);
 
match event {
TrayIconEvent::Click { .. } => {
// then we call position() to build the window.
position(tray_handle.app_handle()).ok();
}
_ => {}
}
})
.build(app)?;
 
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

Your app should have the plugin initialized already because we ran tauri add. Now we just need to add a setup hook. When a tray event is fired, we first pass the app handle and event to the positioner plugin. Then, we handle the click event on our own with a position function, which we'll now implement:

fn position(app: &AppHandle) -> tauri::Result<()> {
let window =
WebviewWindowBuilder::new(app, "position", WebviewUrl::App("position.html".into()))
.decorations(false)
.always_on_top(true)
.skip_taskbar(true)
.build()?;
 
window.move_window(Position::TrayCenter)?;
 
window.clone().on_window_event(move |evt| match evt {
tauri::WindowEvent::Focused(is_focused) if !is_focused => {
window.close().ok();
}
_ => {}
});
Ok(())
}

This function will:

  • Create a new window.
  • With no decorations.
  • Float on top.
  • It won't appear in the

taskbar.

  • Move the window near the taskbar.
  • Then register a window event handler that will close the window when it loses focus.

Now you have all the tools you need to build and manipulate windows using Tauri. There's plenty more you can do, so consult the Rust docs for more. There are JavaScript APIs for some of what we covered, but I found it much easier to work with the Rust side.