Window Appearance

Configure decorations, transparency, shadows, themes, background color, and visibility to control the look and feel of Tauri windows

A Tauri window’s appearance goes beyond just its size and position. You can remove the native title bar, make the window see‑through, disable the drop shadow, pick a light or dark theme, set a startup background colour, or keep the window hidden until it is ready. These settings are all part of the window configuration and can be set in tauri.conf.json or controlled at runtime from Rust or JavaScript.

The choices you make for appearance affect not only how the window looks but also how the user interacts with it. Removing decorations means you need to handle dragging and window controls yourself. Making a window transparent requires careful styling so the content stays readable over whatever is behind the window. This page covers every appearance‑related option, shows complete working examples, and points out the platform‑specific details you need to know.


Decorations

Window decorations are the frame the operating system draws around your window — the title bar, the minimize/maximize/close buttons, and the borders. When decorations is set to false, all of that native chrome disappears. You get a borderless rectangle that you fill entirely with your own UI.

This is the starting point for custom title bars, media players, floating tool palettes, and any design where the window should not look like a standard application.

Disabling decorations in configuration

The simplest way to remove decorations is through the window list in tauri.conf.json:

src-tauri/tauri.conf.json
{
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "My App",
        "width": 800,
        "height": 600,
        "decorations": false
      }
    ]
  }
}

With this change the window opens with no title bar at all. On Linux, the window manager may still draw a shadow or a thin border even when decorations are off — you can combine decorations: false with shadow: false and transparent: true if you need a truly clean rectangle.

No built-in window controls:

A window without decorations cannot be moved, resized, or closed through the OS chrome. You must implement those behaviours yourself if the user needs them.

Creating a custom title bar

A borderless window usually needs its own title bar so the user can drag it and control it. The steps are sequential — you need the config before you write the HTML, and you need the HTML before you wire up the JavaScript.

1

Step 1: Disable decorations

Add "decorations": false to your window configuration as shown above.

2

Step 2: Add window permissions

To call minimize, maximize, and close from the frontend, the window plugin needs the right permissions. Add them to your capability file:

src-tauri/capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-capability",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:window:default",
    "core:window:allow-close",
    "core:window:allow-minimize",
    "core:window:allow-toggle-maximize",
    "core:window:allow-start-dragging"
  ]
}

Without core:window:allow-start-dragging, the drag region will not respond to mouse events.

3

Step 3: Add the title bar HTML

Place a title bar element at the top of your page. The data-tauri-drag-region attribute tells Tauri that dragging this area should move the window. Each button will get an event listener in the next step.

index.html
<div class="titlebar">
  <div data-tauri-drag-region></div>
  <div class="controls">
    <button id="titlebar-minimize" title="minimize">
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
        <path fill="currentColor" d="M19 13H5v-2h14z"/>
      </svg>
    </button>
    <button id="titlebar-maximize" title="maximize">
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
        <path fill="currentColor" d="M4 4h16v16H4zm2 4v10h12V8z"/>
      </svg>
    </button>
    <button id="titlebar-close" title="close">
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
        <path fill="currentColor" d="M13.46 12L19 17.54V19h-1.46L12 13.46L6.46 19H5v-1.46L10.54 12L5 6.46V5h1.46L12 10.54L17.54 5H19v1.46z"/>
      </svg>
    </button>
  </div>
</div>

The data-tauri-drag-region div fills the available space so the user can drag the window by grabbing the title bar between the buttons.

4

Step 4: Style the title bar

The title bar needs to be fixed at the top of the viewport. The rest of the page content should start below it so it is not hidden.

src/styles.css
.titlebar {
  height: 30px;
  background: #329ea3;
  user-select: none;
  display: grid;
  grid-template-columns: auto max-content;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}
.titlebar > .controls {
  display: flex;
}
.titlebar button {
  appearance: none;
  padding: 0;
  margin: 0;
  border: none;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  width: 30px;
  background-color: transparent;
}
.titlebar button:hover {
  background: #5bbec3;
}
5

Step 5: Wire up the buttons

Each button calls the corresponding method on the current window object.

src/main.tsx
import { getCurrentWindow } from "@tauri-apps/api/window";
const appWindow = getCurrentWindow();
document
  .getElementById("titlebar-minimize")
  ?.addEventListener("click", () => appWindow.minimize());
document
  .getElementById("titlebar-maximize")
  ?.addEventListener("click", () => appWindow.toggleMaximize());
document
  .getElementById("titlebar-close")
  ?.addEventListener("click", () => appWindow.close());

If you have built and run the app at this point, you should see a coloured bar at the top with three functional buttons. The rest of the window area is your own content, which you can style independently. The window can be dragged by grabbing the empty space in the title bar.

Everything is working:

When you can drag the window by the custom title bar and all three buttons respond, the decoration and permission setup is correct. The window is now fully under your styling control.

Manual drag handling

The data-tauri-drag-region attribute handles most cases, but if you need finer control — for example, double-click to maximize or a drag region that also contains interactive elements — you can call startDragging manually.

Remove the data-tauri-drag-region attribute from the HTML and attach a mousedown listener instead:

document.getElementById("titlebar")?.addEventListener("mousedown", (e) => {
  if (e.buttons === 1) {
    // Primary (left) button
    e.detail === 2
      ? appWindow.toggleMaximize() // Maximize on double click
      : appWindow.startDragging(); // Else start dragging
  }
});

This gives you full control over when dragging begins. The e.detail check for a double click is a common pattern to replicate the behaviour of native title bars on Windows and Linux.


Transparent Windows

A transparent window has no background fill from the system. Whatever is behind the window shows through any part of your UI that you leave unpainted. This is how floating widgets, overlay tools, and irregularly shaped windows are built.

Enable transparency with the transparent property:

src-tauri/tauri.conf.json
{
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "Transparent App",
        "width": 400,
        "height": 300,
        "decorations": false,
        "transparent": true
      }
    ]
  }
}

macOS requires private API access:

On macOS, the operating system does not expose a public API for transparent window backgrounds. You must set "macOSPrivateApi": true in the app configuration or the window will render with an opaque background regardless of this setting.

src-tauri/tauri.conf.json
{
  "app": {
    "macOSPrivateApi": true,
    "windows": [
      {
        "transparent": true
      }
    ]
  }
}

Making the page actually transparent

Setting transparent: true only tells the window to be transparent. Your HTML document must also have a transparent background — otherwise the browser paints a white background over the window and you see nothing through it.

src/styles.css
html,
body {
  background: transparent;
  margin: 0;
  padding: 0;
}

From here you can place any element with its own background. Only the areas without a background will be see‑through. A common pattern is a rounded floating card:

.card {
  background: rgba(30, 30, 30, 0.92);
  border-radius: 16px;
  padding: 24px;
  color: white;
  margin: 20px;
}

The edges outside the card reveal the desktop behind the window, giving the appearance of a standalone widget.

Platform-specific: macOS transparent title bar with a custom background colour

On macOS you can take transparency further by using a transparent title bar style alongside a custom window background colour set from Rust. This gives a window that looks integrated with the desktop while still having a distinct tint.

First, create the window with title_bar_style(Transparent) and then set the background colour via the NSWindow API:

src-tauri/src/lib.rs
use tauri::{TitleBarStyle, WebviewUrl, WebviewWindowBuilder};
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
                .title("Transparent Titlebar Window")
                .inner_size(800.0, 600.0);
            #[cfg(target_os = "macos")]
            let win_builder = win_builder.title_bar_style(TitleBarStyle::Transparent);
            let window = win_builder.build().unwrap();
            #[cfg(target_os = "macos")]
            {
                use objc2_app_kit::{NSColor, NSWindow};
                let ns_window_ptr = window.ns_window().unwrap() as *mut NSWindow;
                let ns_window = unsafe { &*ns_window_ptr };
                let bg_color = NSColor::colorWithRed_green_blue_alpha(
                    50.0 / 255.0,
                    158.0 / 255.0,
                    163.5 / 255.0,
                    1.0,
                );
                ns_window.setBackgroundColor(Some(&bg_color));
            }
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Remember to add the objc2-app-kit crate to your Cargo dependencies for the macOS target:

src-tauri/Cargo.toml
[target.'cfg(target_os = "macos")'.dependencies]
objc2-app-kit = { version = "0.3.2", features = ["NSColor", "NSWindow", "objc2-core-foundation"] }

The transparent title bar style removes the system‑drawn title bar background, and the custom NSColor tints the remaining window area. The HTML body still needs background: transparent so that the Rust‑side colour is visible.

Transparency is not free:

Compositing a transparent window can be measurably more expensive than an opaque one, especially on integrated graphics. If you do not need the see‑through effect, keep the window opaque for better performance.


Shadows

The shadow property controls whether the operating system draws a drop shadow around the window. It is enabled by default, but you may want to turn it off when building a borderless window that has its own visual style — a system shadow can look out of place around a custom‑shaped UI.

src-tauri/tauri.conf.json
{
  "app": {
    "windows": [
      {
        "decorations": false,
        "shadow": false
      }
    ]
  }
}

Platform behaviour:

  • Windows: The shadow is drawn by the Desktop Window Manager. Disabling it removes the border glow entirely.
  • Linux: The effect depends on the window manager and compositor. On GNOME with Mutter, setting shadow: false will remove the shadow. On some KDE configurations, the window may still show a shadow — in that case you may need to combine it with transparent: true and handle the shape yourself.
  • macOS: The shadow property has no effect. macOS always renders shadows for windows. If you need a shadowless window, you must use a transparent window and draw your own frame.

Shadows on Linux can be stubborn:

On Linux, the window manager may still draw a shadow even when shadow is set to false, particularly when using client‑side decorations (CSD) in GTK‑based environments. The safest way to guarantee no shadow is to create a transparent, undecorated window.


Themes

The theme property sets the preferred colour scheme for the window. It takes one of three values: "light", "dark", or null (the default, which follows the system setting). This affects two things: the appearance of the native title bar on Windows and macOS, and the prefers-color-scheme media query inside the WebView.

src-tauri/tauri.conf.json
{
  "app": {
    "windows": [
      {
        "theme": "dark"
      }
    ]
  }
}

Inside your CSS you can then use the media query to style the content accordingly:

@media (prefers-color-scheme: dark) {
  body {
    background: #1e1e1e;
    color: #e0e0e0;
  }
}
@media (prefers-color-scheme: light) {
  body {
    background: #ffffff;
    color: #1e1e1e;
  }
}

Changing theme at runtime

You can switch themes while the app is running through the frontend API:

import { getCurrentWindow } from "@tauri-apps/api/window";
const appWindow = getCurrentWindow();
// Switch to dark mode
await appWindow.setTheme("dark");
// Switch back to system default
await appWindow.setTheme(null);

The same capability is available on the Rust side via window.set_theme(Some(Theme::Dark)). Changing the theme at runtime immediately updates both the title bar and the WebView’s colour scheme, allowing your app to react live to a user toggle.

Theme affects only the current window:

Each window has its own theme setting. If your app has multiple windows, you must set the theme on each one individually. There is no global app‑wide theme flag.


Background Color

The backgroundColor property sets the window’s initial background colour before the web content paints. It is also the fallback colour if your HTML document has a transparent background and the window is not set to be transparent. The value is a hex string like "#2f2f2f".

src-tauri/tauri.conf.json
{
  "app": {
    "windows": [
      {
        "backgroundColor": "#1a1a2e"
      }
    ]
  }
}

When the app launches, this colour fills the window immediately. As soon as the WebView renders your page, the page’s own background takes over — unless your CSS sets the body to transparent, in which case the system background colour remains visible behind the content.

This is particularly useful for:

  • Avoiding a white flash on startup before your dark‑themed page loads.
  • Providing a tint behind transparent content on macOS when combined with titleBarStyle: Transparent (as shown in the transparent windows section).
  • Giving a consistent base colour to windows that load dynamic content.

You can also change the background colour from Rust after the window is created:

window.set_background_color(Some("#ff0000".parse().unwrap()));

The colour format must be a valid hex string. Invalid formats will be silently ignored by the underlying platform code, so always test that the string parses correctly.

Background color on Windows with transparency:

On Windows, if the window is transparent (transparent: true) and you set a background colour, the colour may not be visible behind the WebView because the compositor treats the window as fully layered. For a tinted transparent window on Windows, apply the colour via CSS instead.


Visibility

The visible property controls whether the window is shown immediately when created. The default is true. Setting it to false hides the window until you explicitly call show().

src-tauri/tauri.conf.json
{
  "app": {
    "windows": [
      {
        "visible": false
      }
    ]
  }
}

This is essential in two common scenarios:

  • Splash screens: You create the main window hidden, display a lightweight splash window, load the full UI in the background, then show the main window and destroy the splash.
  • Multi‑window coordination: You create several windows hidden, set up their content and positions, then show them all at once so the user sees a fully arranged layout rather than windows popping into existence one by one.

From Rust, creating a hidden window and later showing it looks like this:

use tauri::{WebviewUrl, WebviewWindowBuilder};
let window = WebviewWindowBuilder::new(app, "secondary", WebviewUrl::default())
    .visible(false)
    .build()?;
// ... load content or set up state ...
window.show()?;

From the frontend, you can hide or show an already‑existing window:

import { getCurrentWindow } from "@tauri-apps/api/window";
const appWindow = getCurrentWindow();
await appWindow.hide();
// later...
await appWindow.show();

A hidden window is not a background window:

A window with visible: false still consumes memory and CPU. If you are done with a window, call close() to release its resources. Hiding is for temporary concealment, not for window lifecycle management.

The focus property, which defaults to true, controls whether the window grabs keyboard focus when it first appears. If you create a hidden window and later show it, it will still gain focus at that moment unless you also call set_focus() to control focus explicitly.


Summary

Window appearance in Tauri v2 is a collection of independent knobs — decorations, transparency, shadows, themes, background colour, and visibility — that together define how a window presents itself to the user. Each setting has a clear default and a specific set of platform constraints you need to account for, especially on macOS where private APIs gate transparency and on Linux where the window manager can override shadow and decoration behaviour.

The most common configuration starts with disabling decorations, building a custom title bar, and then layering on transparency or theme changes as the design demands. The same properties that define the initial look can also be changed at runtime through both the Rust and JavaScript APIs, letting the window evolve in response to user actions.