Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tauri Snap Layouts — Frameless Windows with a Custom Title Bar on Windows 11

Design your own title bar. Keep Windows working.

Build a desktop app in Rust with your own custom title bar — and still get the Windows 11 snap menu, rounded corners, and every window behaviour users expect.

Two templates you can copy and run today.

License: MIT Tauri v2 Rust Windows 11 Verified


Windows 11 Snap Layouts flyout appearing over a frameless Tauri window with a custom tab strip

That menu is the whole point. It is a real Windows 11 Snap Layouts flyout, on a window whose title bar is entirely our own HTML.


The problem, in one sentence

The moment you set "decorations": false in Tauri to design your own title bar, Windows quietly stops offering the snap menu when users hover the maximize button — and nothing you write in HTML, CSS or JavaScript can bring it back.

Most people conclude it's impossible in Rust and reach for Electron. It isn't, and you don't have to. This is not a workaround either — it is the same mechanism Windows Terminal, VS Code and Electron itself use, and it is roughly 380 lines of Win32 you can read.

This repo is the working answer, the two templates, and the map of every trap.

What you get

The frameless-window template: one continuous canvas with window controls floating in the corner

frameless-window/

One clean canvas. The whole window is yours from pixel (0,0) — no header, no reserved strip. The minimize, maximize and close buttons float in a 276×32 cluster in the corner.

Start here for most apps.

The frameless-window-tabs template: a tab strip sharing one row with the window controls

frameless-window-tabs/

A Windows Terminal-style tab strip sharing one row with the caption buttons. Rename by double-clicking, tabs crowd down to icon-only, overflow menu, keyboard shortcuts.

Start here for anything document-based.

Both give you, with no extra work:

  • ✅ The Windows 11 Snap Layouts flyout on maximize hover
  • Rounded corners that match the rest of the OS
  • Resizing from every edge and corner
  • ✅ Drag, double-click-to-maximize, Win+Arrow, Aero Shake, taskbar previews
  • ✅ Your own design, top to bottom

Try it

git clone https://github.com/Zbrooklyn/tauri-snap-layouts
cd tauri-snap-layouts/frameless-window
npm install
npm run dev

Requires Rust with the MSVC toolchain, Visual Studio Build Tools ("Desktop development with C++"), WebView2 (preinstalled on Windows 11), and Node 18+ for the Tauri CLI. Neither template uses a bundler.

Proof, not promises

Run pwsh -File .\verify.ps1 and it checks the real running window — not a mock:

frameless-window
  PASS  snap overlay    46x32px on the maximize button, 0px drift
  PASS  resizable       all 6 probed edges and corners answer resize hit-tests
  PASS  corners         DWMWCP_ROUND
  PASS  minimize
  PASS  maximize
  PASS  close
The window's top-left corner at 8x zoom, showing a genuinely rounded and antialiased edge
Top-left corner at 8×. Magenta is the desktop showing through — the corner is genuinely rounded, not painted.

Does this describe your problem?

If you searched for any of these, you are in the right place — the answer is in the sections below.

  • Tauri snap layouts not working with decorations: false
  • Tauri custom titlebar loses the Windows 11 snap layout flyout
  • Snap Layouts don't show on hover over my custom maximize button
  • HTMAXBUTTON / WM_NCHITTEST — how do I return it from a Tauri or winit window?
  • winit / tao cannot return HTMAXBUTTON (winit#3884) — is Tauri blocked forever?
  • Tauri maximize button does nothing / doesn't maximize when clicked
  • Tauri maximize button has no hover effect / CSS :hover doesn't fire on it
  • Snap layout flyout appears but the window won't snap into a zone
  • Tauri window rounded corners on Windows 11 / DWMWA_WINDOW_CORNER_PREFERENCE
  • Can I set a custom window corner radius in Tauri (or Electron)?
  • Is a frameless Tauri window still resizable?
  • data-tauri-drag-region — do I have to give up the whole top edge?
  • WebView2 child window intercepts WM_NCHITTEST — how do I get around it?
  • cargo build fails with os error 32 in a Dropbox / OneDrive folder
  • Electron does this, can Rust? — yes, and this repo proves it

Short answer to the big one: Tauri issue #4531 is labelled status: upstream, and almost everyone reads that as "impossible until winit changes." That is the single most expensive misreading in this problem space. Upstream is blocked from shipping a first-class API. Nothing stops your app from creating its own Win32 window, and that works today.


Going deeper

Everything below is the technical detail. You don't need it to use the templates — but if something breaks, or you want to build this yourself instead of copying, the answer is here.

Why does hiding the title bar break the snap menu? (the architecture)

Windows shows the Snap Layouts flyout only when a window answers the WM_NCHITTEST message with HTMAXBUTTON. That is a Win32 return value from a window procedure — there is no DOM, CSS, or Tauri API that can produce it.

Worse, in Tauri your page lives in a WebView2 child window (Chrome_RenderWidgetHostHWND) that covers the entire client area. When the cursor is over your maximize button, Windows asks that window what is there. Your Tauri window's procedure is never consulted. So even implementing the hit-test correctly — exactly as Microsoft documents it — does nothing.

The fix: create a small transparent native child window sitting precisely over your HTML maximize button, whose window procedure returns HTMAXBUTTON unconditionally. It never paints, so your design shows through, but it owns the mouse in that rectangle — which means it must report hover and clicks back to your page.

Five independent plugins converged on this identical technique, which is strong evidence there is no other route.

Full explanation, four approaches ranked, and every source: docs/WINDOWS-FRAMELESS-SNAP-LAYOUTS.md

TL;DR — the actual fix, in code
// src-tauri/src/lib.rs
tauri::Builder::default()
    .plugin(
        tauri_plugin_frame::FramePluginBuilder::new()
            .auto_titlebar(false)     // you draw the controls
            .snap_overlay(true)
            .titlebar_height(32)      // MUST match your CSS
            .button_width(46)         // MUST match your CSS
            .build(),
    )
    .setup(|app| {
        use tauri::Manager;
        use tauri_plugin_frame::WebviewWindowExt;
        let window = app.get_webview_window("main").unwrap();
        window.create_overlay_titlebar_with_height(32)?;  // the overlay attaches HERE
        Ok(())
    })
// The overlay covers the button, so its onclick and :hover never fire.
// It emits these instead and expects you to act on them.
const { listen } = window.__TAURI__.event;
const appWindow = window.__TAURI__.window.getCurrentWindow();

listen("tauri-frame://snap/click",      () => appWindow.toggleMaximize());
listen("tauri-frame://snap/mouseenter", () => maxBtn.classList.add("cursor-over"));
listen("tauri-frame://snap/mouseleave", () => maxBtn.classList.remove("cursor-over"));
// tauri.conf.json — minWidth is a Microsoft requirement, not a style choice
{ "decorations": false, "minWidth": 330 }

For rounded corners, both templates call DwmSetWindowAttribute with DWMWA_WINDOW_CORNER_PREFERENCE = DWMWCP_ROUND (2). Change CORNER_PREFERENCE in lib.rs to 1 for square or 3 for a tighter radius.

The five traps that cost the most time

1. Your button ids can silently switch the plugin off. tauri-plugin-frame injects a script that begins:

if (!tbEl || tbEl.querySelector("[id^='frame-tb-']")) return;

If your title bar already contains any frame-tb-* element, the entire setup returns and registers nothing — no click handler, no hover, no snap listeners. Those ids are the plugin's own, published in its README. Hand-writing your buttons with the documented ids is exactly what breaks it. Symptom: the flyout works, minimize and close work, and the maximize button does nothing while looking perfectly alive.

2. The overlay is positioned by arithmetic, not by measuring your DOM.

x = client_right − button_width × (buttons_to_the_right + 1)

So titlebar_height / button_width in Rust must equal your CSS, all caption buttons must be the same width, and nothing may sit to the right of close. Drift = the flyout silently stops appearing and nothing else breaks.

3. minWidth above ~500px means the flyout appears but the window won't snap. Microsoft asks for ≤500 effective pixels, ideally ≤330. Almost nobody sets this, and the symptom looks like a half-broken feature.

4. Subclassing the Tauri window to answer WM_NCHITTEST is structurally impossible. It is literally the code in Microsoft's documentation, and it cannot work here — the WebView2 child window answers first. You can recognise this trap by your button still showing its CSS :hover and its HTML title tooltip; both require the webview to have received the mouse, which means your window did not.

5. WS_CLIPSIBLINGS is load-bearing if you roll your own overlay. The working style set is WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_OVERLAPPED, no extended styles, a NULL_BRUSH background, and SWP_ASYNCWINDOWPOS | SWP_SHOWWINDOW. Invisibility comes from never painting, not from WS_EX_LAYERED — layering costs you the hit test.

FAQ — every question people actually ask

Why doesn't my Tauri app show Snap Layouts with a custom titlebar?

Because Windows only offers Snap Layouts to a window that answers WM_NCHITTEST with HTMAXBUTTON, and your webview — a child window covering the client area — answers that hit-test before your window can. See the architecture section.

Is this blocked upstream in winit / tao?

A first-class Tauri API is. Your app is not. winit#3884 is real and open, and tauri#4531 is labelled status: upstream — but you can create your own Win32 window alongside Tauri's, and that is what every working solution does.

Which plugin should I use?

Five independent plugins converged on the identical technique, which is strong evidence there is no other route: tauri-plugin-frame (used here), tauri-plugin-decorum, tauri-plugin-window-controls, tauri-plugin-decoration, and tauri-plugin-snap-layout. Or vendor the ~380 lines yourself.

Why does my maximize button not respond to clicks?

Almost certainly trap #1 above — your frame-tb-* ids made the plugin skip its own setup. The overlay also covers the button, so a plain onclick will never fire; you must listen for tauri-frame://snap/click.

Why does CSS :hover not work on my maximize button?

The native overlay owns the mouse in that rectangle, so the webview never sees it. Drive a class from tauri-frame://snap/mouseenter and mouseleave instead.

Can I have rounded corners on a Tauri window on Windows 11?

Yes. Call DwmSetWindowAttribute with DWMWA_WINDOW_CORNER_PREFERENCE = DWMWCP_ROUND (both templates do). Note Microsoft's wording: the API is "a hint to the system and does not guarantee rounding", and windows using per-pixel alpha or window regions "cannot ever be rounded".

Can I set a custom corner radius, like 14px?

Not through a supported API — and not in Electron either, which surprises people. Electron's roundedCorners is a boolean, Windows support only landed in Electron v34.3.0, and the request to customise the radius (electron#47833) was closed as not planned. Underneath, it is the same DWM attribute. The unsupported route is a transparent window plus CSS border-radius, which has a long history of corners rendering opaque on Windows (electron#22243, tauri#3481).

Does CSS border-radius round the window?

Not on an opaque window — the page is painted inside a rectangle, so the radius just shows whatever is behind it in the page. On a "transparent": true window it can, with four gotchas; the biggest is that html must be transparent as well as body.

Is a frameless Tauri window still resizable?

Yes, with no extra code. decorations: false removes the caption, not the resize frame. Verified by sending WM_NCHITTEST to a live window: all eight edges and corners answer correctly. There is an invisible ~8px grab border, which is why outerPosition() and innerPosition() disagree.

Do I have to make the whole top edge a drag region?

No. data-tauri-drag-region can be any size, anywhere. In frameless-window/ it is 138×32 in the corner, leaving ~69% of the top edge as live, clickable canvas. Only the maximize button's position is genuinely constrained, because the native overlay is anchored to the top-right corner.

Why does cargo build fail with os error 32?

A cloud-sync client (Dropbox, OneDrive, iCloud, Google Drive) is locking files inside target/ mid-build. Both templates ship a .cargo/config.toml that moves target-dir off the synced folder. It must sit next to Cargo.toml — cargo walks up from the current directory.

How do I prove it actually works?

pwsh -File .\verify.ps1. It asserts against the OS rather than screenshotting, because PrintWindow cannot capture the flyout — the flyout is a separate OS window, so screenshot methods produce confident false failures. That mistake is documented in §6.


What's in the repo

Path What it is
frameless-window/ Template 1. One canvas — the whole window is yours from pixel (0,0). No header, no reserved row; the caption buttons float in a 276×32 cluster in the corner.
frameless-window-tabs/ Template 2. A Windows Terminal-style tab strip sharing one row with the caption buttons: rename, crowding to icon-only, overflow menu, keyboard shortcuts.
docs/WINDOWS-FRAMELESS-SNAP-LAYOUTS.md The full reference. Why it's hard, four approaches ranked, the complete failure catalogue, what the public sources get wrong, and how to verify.
verify.ps1 Proves the snap overlay is hit-testing the maximize button, the window is resizable, corners are set, and all three caption buttons work.
tools/wincap/ A Rust Windows.Graphics.Capture tool that screenshots a window with its alpha channel, through occlusion. The only way to visually prove rounded corners.

Verified environment

Windows 11 build 26200 · Tauri 2.11.5 · tauri-plugin-frame 1.1.8 · Rust 1.97 MSVC · WebView2 Evergreen.

verify.ps1 passes all six checks on both templates — snap overlay, resizable, rounded corners, minimize, maximize, close. The overlay measures 46×32 px at 100% scaling and 69×48 at 150%, with 0px drift at both, which is the DPI scaling behaving correctly rather than a value that happens to work on one machine.

Contributing

If you hit a failure mode not covered here, open an issue with your Windows build, Tauri version and scaling factor. The goal is for this to be the page that ends the search.

License

MIT — see LICENSE.

About

Working Windows 11 Snap Layouts in a frameless Tauri v2 app with a custom titlebar. Two runnable Rust templates + the full write-up of why HTMAXBUTTON/WM_NCHITTEST fails in a webview, and how to fix it.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages