← Blog
Making a Floating Window Stay on Top of Full-Screen Apps on macOS

Making a Floating Window Stay on Top of Full-Screen Apps on macOS

TauriRustMacos

While building KeliKeli — a lightweight keystroke counter that floats on your screen — I ran into a surprisingly tricky macOS problem: the floating window would disappear the moment you entered a full-screen app.

The fix sounds simple on paper. In practice, it requires understanding a subtle interaction between macOS window levels, collection behaviors, and activation policies — and getting the order of operations right.

The Problem

KeliKeli's floating indicator is meant to always be visible. Setting always_on_top: true in Tauri's config works fine in normal windowed environments, but full-screen apps on macOS run in their own dedicated Space. Any window without the right collection behavior flags simply doesn't follow you there — it stays behind, invisible.

What Didn't Work

The first instinct was to set NSWindowCollectionBehaviorCanJoinAllSpaces on the window. That flag tells macOS the window should appear across all Spaces, including full-screen ones. But it wasn't enough on its own.

The deeper issue: macOS only honors CanJoinAllSpaces for windows belonging to apps with the Accessory activation policy (equivalent to setting LSUIElement = true in Info.plist). A regular app-policy window, even with the flag set, won't render above a full-screen app's Space.

Crucially, the activation policy must be set before any window is created. If you apply it after the fact, the NSWindow has already inherited the wrong policy and the behavior flags have no effect.

The Fix

The solution involved three coordinated changes:

1. Set activation policy first

In the Tauri setup callback — before creating any window — set the activation policy:

#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);

This is equivalent to LSUIElement = true but done programmatically, which is necessary because the window must be created after the policy is applied.

2. Create the window programmatically

Instead of declaring the window in tauri.conf.json (where it would be created before setup runs), build it manually inside setup:

let win = tauri::WebviewWindowBuilder::new(
    app,
    "main",
    tauri::WebviewUrl::App("index.html".into()),
)
.title("KeliKeli")
.inner_size(72.0, 80.0)
.resizable(false)
.decorations(false)
.transparent(true)
.always_on_top(true)
.accept_first_mouse(true)
.visible(true)
.build()?;

The windows array in tauri.conf.json is left empty.

3. Set window level and collection behavior via raw Objective-C

Once the window exists, drop into unsafe Objective-C to apply the final flags:

// Level 10000: high enough to appear above full-screen apps on all Spaces
let _: () = msg_send![ns_window, setLevel: 10000i64];

// Read existing behavior first, then OR in our flags to preserve Tauri's defaults
// CanJoinAllSpaces (1) | Stationary (16) | FullScreenAuxiliary (256)
let existing: usize = msg_send![ns_window, collectionBehavior];
let _: () = msg_send![ns_window, setCollectionBehavior: existing | 1 | 16 | 256];

Reading the existing collectionBehavior before ORing in the new flags is important — Tauri sets some behavior flags of its own, and overwriting them entirely can break other functionality.

The three flags combined:

  • CanJoinAllSpaces (1) — appear on every Space, including full-screen ones
  • Stationary (16) — don't move or animate when Spaces transition
  • FullScreenAuxiliary (256) — render alongside (not behind) full-screen apps

4. Ship an Info.plist

As a belt-and-suspenders measure, an Info.plist with LSUIElement = true is included and referenced in tauri.conf.json. This ensures the accessory policy is always applied, even if the programmatic call somehow runs too late in an edge case.

The Key Insight

The order of operations is everything. macOS bakes certain window properties at creation time based on the app's activation policy. By the time you try to set CanJoinAllSpaces, it's too late if the window already exists under a regular policy.

The working sequence is: set policy → create window → apply flags.

Getting this right means KeliKeli now stays visible no matter what's on screen — whether you're in a full-screen terminal, a browser in fullscreen, or switching Spaces. The indicator just follows you everywhere, exactly as intended.