<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Edward Shamosh</title>
    <description>The latest articles on DEV Community by Edward Shamosh (@zbrooklyn).</description>
    <link>https://dev.to/zbrooklyn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4053622%2F2d69a72d-4d10-46b4-ab5e-4015797e0f74.png</url>
      <title>DEV Community: Edward Shamosh</title>
      <link>https://dev.to/zbrooklyn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zbrooklyn"/>
    <language>en</language>
    <item>
      <title>Windows 11 Snap Layouts in a frameless Tauri app (the part every guide gets wrong)</title>
      <dc:creator>Edward Shamosh</dc:creator>
      <pubDate>Wed, 29 Jul 2026 16:39:19 +0000</pubDate>
      <link>https://dev.to/zbrooklyn/windows-11-snap-layouts-in-a-frameless-tauri-app-the-part-every-guide-gets-wrong-2fjh</link>
      <guid>https://dev.to/zbrooklyn/windows-11-snap-layouts-in-a-frameless-tauri-app-the-part-every-guide-gets-wrong-2fjh</guid>
      <description>&lt;p&gt;I had developers try this and fail. Twice. The conclusion both times was the same: "if you need a custom title bar that doesn't break Windows, use Electron."&lt;/p&gt;

&lt;p&gt;That conclusion is wrong, and I want to lay out exactly why — because the reason it's wrong is buried under a label on a GitHub issue that almost everyone misreads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;"decorations": false&lt;/code&gt; in Tauri so you can draw your own title bar. Everything looks great. Then a user hovers your maximize button and the Windows 11 snap menu — the little grid of layout options — doesn't appear.&lt;/p&gt;

&lt;p&gt;You lose it silently. No error, no warning. And nothing you write in HTML, CSS, or JavaScript can bring it back.&lt;/p&gt;

&lt;p&gt;That last part is the bit that breaks people's mental model, so it's worth being precise about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why no amount of frontend code can fix this
&lt;/h2&gt;

&lt;p&gt;Windows only offers Snap Layouts to a window that answers the &lt;code&gt;WM_NCHITTEST&lt;/code&gt; message with &lt;code&gt;HTMAXBUTTON&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's a Win32 return value from a window procedure. There is no DOM API that produces it. No CSS property. No Tauri config key. Your frontend is simply not a participant in this conversation.&lt;/p&gt;

&lt;p&gt;So you go and implement the hit test properly, exactly as &lt;a href="https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/apply-snap-layout-menu" rel="noopener noreferrer"&gt;Microsoft documents it&lt;/a&gt;… and nothing happens.&lt;/p&gt;

&lt;p&gt;Here's why. In Tauri your page lives inside a &lt;strong&gt;WebView2 child window&lt;/strong&gt; that covers the entire client area. When the cursor is over your maximize button, Windows asks &lt;em&gt;that&lt;/em&gt; window what's underneath. Your Tauri window's procedure never gets consulted.&lt;/p&gt;

&lt;p&gt;You can recognise you're in this trap by a very specific tell: &lt;strong&gt;your maximize button still shows its CSS &lt;code&gt;:hover&lt;/code&gt; state and its HTML tooltip.&lt;/strong&gt; Both of those require the webview to have received the mouse — which means your window did not.&lt;/p&gt;

&lt;p&gt;And subclassing doesn't rescue you either. Chromium uses its own internal input routing, so &lt;code&gt;SetWindowSubclass&lt;/code&gt; fails on exactly the descendant windows that actually receive the mouse. Force-swapping its WndProc breaks rendering.&lt;/p&gt;

&lt;p&gt;I watched another team reach this same conclusion independently, in a different language, two weeks ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  The misreading that costs everyone a week
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tauri-apps/tauri/issues/4531" rel="noopener noreferrer"&gt;Tauri issue #4531&lt;/a&gt; is labelled &lt;code&gt;status: upstream&lt;/code&gt;. &lt;a href="https://github.com/rust-windowing/winit/issues/3884" rel="noopener noreferrer"&gt;winit#3884&lt;/a&gt; says plainly that returning &lt;code&gt;HTMAXBUTTON&lt;/code&gt; isn't possible there, with no workaround.&lt;/p&gt;

&lt;p&gt;Both statements are true. &lt;strong&gt;Both are about the framework shipping a first-class API.&lt;/strong&gt; Neither says anything about what your application can do.&lt;/p&gt;

&lt;p&gt;Nothing stops you from creating your own Win32 window alongside Tauri's. That's the whole fix. Reading &lt;code&gt;status: upstream&lt;/code&gt; as "impossible until winit changes" is, as far as I can tell, the single most expensive misreading in this problem space — and it's why a four-year-old thread is still full of people concluding they should switch to Electron.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual fix
&lt;/h2&gt;

&lt;p&gt;Create a small &lt;strong&gt;transparent native child window&lt;/strong&gt; sitting exactly over your HTML maximize button, whose window procedure returns &lt;code&gt;HTMAXBUTTON&lt;/code&gt; unconditionally.&lt;/p&gt;

&lt;p&gt;It never paints, so your design shows through. But it genuinely owns the mouse in that rectangle — which means it has to report hover and clicks back to your page, or your button goes visually dead.&lt;/p&gt;

&lt;p&gt;Five independent plugins converged on this identical technique. That's decent evidence there's no other route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;tauri_plugin_frame&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;FramePluginBuilder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.auto_titlebar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// you draw the controls&lt;/span&gt;
            &lt;span class="nf"&gt;.snap_overlay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.titlebar_height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// MUST match your CSS&lt;/span&gt;
            &lt;span class="nf"&gt;.button_width&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;// MUST match your CSS&lt;/span&gt;
            &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.setup&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.get_webview_window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.create_overlay_titlebar_with_height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The overlay covers the button, so onclick and :hover never fire.&lt;/span&gt;
&lt;span class="c1"&gt;// It emits these instead.&lt;/span&gt;
&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tauri-frame://snap/click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;appWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggleMaximize&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tauri-frame://snap/mouseenter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cursor-over&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tauri-frame://snap/mouseleave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cursor-over&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Five traps that aren't written down anywhere
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Your button IDs can silently switch the plugin off.&lt;/strong&gt; &lt;code&gt;tauri-plugin-frame&lt;/code&gt; injects a script beginning &lt;code&gt;if (!tbEl || tbEl.querySelector("[id^='frame-tb-']")) return;&lt;/code&gt;. If your title bar already has any &lt;code&gt;frame-tb-*&lt;/code&gt; element, the whole setup bails and registers nothing. Those IDs are the plugin's own, published in its README — so hand-writing your buttons with the documented IDs is precisely what breaks it. Symptom: the flyout works, minimize and close work, and the maximize button does nothing while looking completely alive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The overlay is positioned by arithmetic, not by measuring your DOM.&lt;/strong&gt; &lt;code&gt;x = client_right − button_width × (buttons_to_the_right + 1)&lt;/code&gt;. Your Rust values must equal your CSS exactly. Any drift and the flyout silently stops appearing while nothing else breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;minWidth&lt;/code&gt; above ~500px means the flyout appears but the window won't snap.&lt;/strong&gt; Microsoft asks for ≤500 effective pixels, ideally ≤330. Almost nobody sets this, and it reads like a half-broken feature rather than a config value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Subclassing is structurally impossible, not just hard.&lt;/strong&gt; Covered above — but worth restating, because it's the documented approach and people lose days to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. &lt;code&gt;WS_CLIPSIBLINGS&lt;/code&gt; is load-bearing if you roll your own.&lt;/strong&gt; Working set: &lt;code&gt;WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_OVERLAPPED&lt;/code&gt;, &lt;strong&gt;no&lt;/strong&gt; extended styles, &lt;code&gt;NULL_BRUSH&lt;/code&gt;. Invisibility must come from never painting. &lt;code&gt;WS_EX_LAYERED&lt;/code&gt; costs you the hit test, and &lt;code&gt;WS_EX_TRANSPARENT&lt;/code&gt; makes the window hit-test-transparent — which defeats the entire point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson that surprised me most
&lt;/h2&gt;

&lt;p&gt;More often than the implementation was broken, &lt;strong&gt;my instrument was blind.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I spent a long time convinced rounded corners weren't working. They were. &lt;code&gt;PrintWindow&lt;/code&gt; renders a window's &lt;em&gt;own&lt;/em&gt; pixels and is completely blind to DWM compositing — so it cheerfully showed me square corners on a window that was genuinely rounded. It also can't capture the Snap Layouts flyout at all, because the flyout is a separate OS window.&lt;/p&gt;

&lt;p&gt;Screenshot-based verification of window chrome produces confident false failures. The answer was Windows.Graphics.Capture — the API Snipping Tool and OBS use — which reads the visual out of DWM and preserves the alpha channel. An antialiased alpha ramp from 9 to 255 across six pixels at (0,0) &lt;em&gt;is&lt;/em&gt; a rounded corner. A square window reads 255 flat.&lt;/p&gt;

&lt;p&gt;If you're debugging window chrome and your evidence disagrees with your code, suspect the camera before the subject.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take the templates
&lt;/h2&gt;

&lt;p&gt;I put up two runnable templates — a plain one-canvas shell, and a Windows Terminal-style tab strip sharing the row with the caption buttons — plus rounded corners, a full failure catalogue, and a &lt;code&gt;verify.ps1&lt;/code&gt; that asserts against a live window instead of trusting a screenshot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Zbrooklyn/tauri-snap-layouts" rel="noopener noreferrer"&gt;https://github.com/Zbrooklyn/tauri-snap-layouts&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/Zbrooklyn/tauri-snap-layouts&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tauri-snap-layouts/frameless-window&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verified on Windows 11 build 26200, Tauri 2.11.5, at both 100% and 150% scaling. MIT licensed.&lt;/p&gt;

&lt;p&gt;If you hit a failure mode I haven't documented, open an issue — the goal is for that repo to be the page that ends the search.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>windows</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
