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.
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 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.
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
git clone https://github.com/Zbrooklyn/tauri-snap-layouts
cd tauri-snap-layouts/frameless-window
npm install
npm run devRequires 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.
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
Top-left corner at 8×. Magenta is the desktop showing through — the corner is genuinely rounded, not painted.
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
:hoverdoesn'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 buildfails withos error 32in 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.
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"));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
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.
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.
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.
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.
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.
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".
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).
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.
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.
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.
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.
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.
| 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. |
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.
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.
MIT — see LICENSE.

