← Blog

Transparent Windows in Tauri v2 on macOS: The Complete Guide

TauriRustMacosDesktop

Getting a truly transparent window in a Tauri v2 app on macOS is deceptively tricky. The config option alone won't cut it — you need three layers working in concert: the Tauri config, CSS, and a Rust shim that talks directly to AppKit. Here's everything I learned the hard way.

Why Is This So Hard?

macOS renders windows through multiple native layers. A Tauri window is actually a stack: an NSWindow, inside which lives a WKWebView, inside which runs your web content. Each layer has its own background. Making the window "transparent" means making all three transparent simultaneously — and each one requires a different approach.

Step 1: tauri.conf.json — Three Fields, All Required

{
  "app": {
    "macOSPrivateApi": true
  },
  "windows": [{
    "transparent": true,
    "decorations": false,
    "alwaysOnTop": true
  }]
}

The critical one is macOSPrivateApi: true. Without it, macOS simply ignores the transparency request — transparent: true alone does nothing. This flag unlocks system-level compositing for the window.

Note: macOSPrivateApi uses a private Apple API. Apps distributed via the App Store will be rejected. For personal or internal tooling, it's fine.

Step 2: CSS — Every Layer Must Be Transparent

html, body, #root {
  background: transparent;
}

Miss any one of these three selectors and you'll get a white bleed-through. The browser compositor treats html, body, and your app root as independent paint surfaces.

Step 3: Rust — Reach Into NSWindow Directly

Even after steps 1 and 2, the WKWebView native layer may still paint an opaque background. The fix is to set it explicitly via Objective-C at runtime.

Add to Cargo.toml:

[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2"

Then in lib.rs setup:

#[cfg(target_os = "macos")]
fn make_webview_transparent(win: &tauri::WebviewWindow) {
    use objc::runtime::{Class, Object, NO};
    use objc::{msg_send, sel, sel_impl};

    if let Ok(ptr) = win.ns_window() {
        unsafe {
            let ns_window = ptr as *mut Object;
            let clear: *mut Object =
                msg_send![Class::get("NSColor").unwrap(), clearColor];
            let _: () = msg_send![ns_window, setOpaque: NO];
            let _: () = msg_send![ns_window, setBackgroundColor: clear];
            // Apply the same to contentView and subviews if needed
        }
    }
}

Call this function during window setup, after the window is created. This reaches past Tauri's abstraction layer and sets the native NSWindow background to clearColor with setOpaque: NO.

Common Pitfalls

Problem Root Cause
Window is still white after transparent: true macOSPrivateApi: true is missing — macOS requires it to unlock system-level transparency
CSS is transparent but window background is still white The WKWebView is a separate native layer; CSS alone cannot control it
set_background_color(Color(0,0,0,0)) has no effect This only affects NSWindow, not WKWebView itself
Drag region not working data-tauri-drag-region requires the core:window:allow-start-dragging permission, or use the startDragging() API instead

Summary

True macOS window transparency in Tauri v2 requires:

  1. macOSPrivateApi: true in your Tauri config to unlock the system compositor
  2. Transparent CSS on html, body, and #root
  3. A Rust + objc shim to zero out the native NSWindow and WKWebView backgrounds at runtime

All three are non-negotiable. Skip any one of them and you'll be staring at a white rectangle wondering what went wrong.