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 anErr
. - 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 useeffects.html
instead ofindex
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).
- We use the
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.