Window Basics
Learn the fundamentals of window management.
Wails provides frameless window support with CSS-based drag regions and platform-native behaviour. Remove the platform-native title bar for complete control over window chrome, custom designs, and unique user experiences whilst maintaining essential functionality like dragging, resizing, and system controls.
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Frameless App", Width: 800, Height: 600, Frameless: true,})CSS for draggable title bar:
.titlebar { --wails-draggable: drag; height: 40px; background: #333;}
.titlebar button { --wails-draggable: no-drag;}HTML:
<div class="titlebar"> <span>My Application</span> <button onclick="window.close()">×</button></div>That’s it! You have a custom title bar.
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Frameless: true, Width: 800, Height: 600,})What you get:
What you need to implement:
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Frameless: true, BackgroundType: application.BackgroundTypeTransparent,})Use cases:
Use the --wails-draggable CSS property:
/* Draggable area */.titlebar { --wails-draggable: drag;}
/* Non-draggable elements within draggable area */.titlebar button { --wails-draggable: no-drag;}Values:
drag - Area is draggableno-drag - Area is not draggable (even if parent is)<div class="titlebar"> <div class="title">My Application</div> <div class="controls"> <button class="minimize">−</button> <button class="maximize">□</button> <button class="close">×</button> </div></div>.titlebar { --wails-draggable: drag; display: flex; justify-content: space-between; align-items: center; height: 40px; background: #2c2c2c; color: white; padding: 0 16px;}
.title { font-size: 14px; user-select: none;}
.controls { display: flex; gap: 8px;}
.controls button { --wails-draggable: no-drag; width: 32px; height: 32px; border: none; background: transparent; color: white; font-size: 16px; cursor: pointer; border-radius: 4px;}
.controls button:hover { background: rgba(255, 255, 255, 0.1);}
.controls .close:hover { background: #e81123;}JavaScript for buttons:
import { Window } from '@wailsio/runtime'
document.querySelector('.minimize').addEventListener('click', () => Window.Minimise())document.querySelector('.maximize').addEventListener('click', () => Window.Maximise())document.querySelector('.close').addEventListener('click', () => Window.Close())Windows can treat parts of a custom title bar as native non-client areas. This lets you draw the title bar and caption buttons with any HTML/CSS design while keeping native Windows behavior: the caption area drags the window, the maximize button can show Windows 11 Snap Assist / Snap Layouts, and the minimize, maximize, and close buttons receive native hit testing and mouse state.
The video below shows a custom HTML/CSS title bar using native Windows hit testing, including Windows 11 Snap Assist / Snap Layouts on a custom maximize button.
Wails supports two Windows-specific mechanisms:
app-region through WebView2’s native non-client region support--wails-non-client-region through Wails runtime tracking for custom caption buttonsChoose based on what you need from Windows:
NonClientRegionSupport for simple native app dragging with WebView2’s app-region: drag and app-region: no-drag.WebView2CompositionHosting when your custom minimize, maximize, and close buttons should behave like native Windows caption buttons.app-region support and Wails-managed custom caption-button regions.NonClientRegionSupport is the lightweight native alternative to Wails’ --wails-draggable tracking. You mark draggable and non-draggable areas with CSS, WebView2 decides which pixels belong to the caption, and Wails asks WebView2 for the native region when hit testing.
That is the full scope of this mode today. It does not make custom minimize, maximize, or close buttons behave like native Windows caption buttons, and it does not enable Windows 11 Snap Assist / Snap Layouts for a custom maximize button. Use it when you need simple native app dragging without the extra machinery of --wails-draggable.
WebView2CompositionHosting is for custom caption buttons with native behavior. Wails tracks DOM rectangles marked with --wails-non-client-region, maps them to Windows hit-test values such as HTMINBUTTON, HTMAXBUTTON, and HTCLOSE, and forwards mouse input back into the composition-hosted WebView2 surface. That is what allows a custom maximize button to participate in Windows 11 Snap Assist / Snap Layouts while keeping any visual design you choose.
Put another way: NonClientRegionSupport is WebView2-native CSS region support. WebView2CompositionHosting is Wails taking responsibility for host-owned composition and custom non-client hit testing.
Enable WebView2’s native non-client region support for the window:
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Frameless: true, Windows: application.WindowsWindow{ NonClientRegionSupport: true, },})Then mark draggable areas with the CSS app-region property:
.titlebar { app-region: drag;}
.titlebar button,.titlebar input,.titlebar select,.titlebar textarea { app-region: no-drag;}Use this when you only need native caption dragging and your title bar controls are handled with normal frontend clicks.
The limitation of this mode is that it is constrained by WebView2’s own non-client region support. In current WebView2 releases, that means drag and no-drag regions only. It is not intended to model fully custom frontend caption buttons with distinct native minimize, maximize, and close roles.
For custom minimize, maximize, and close buttons that should behave like system caption buttons, enable composition hosting:
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Frameless: true, Windows: application.WindowsWindow{ WebView2CompositionHosting: true, },})Then mark each frontend region with --wails-non-client-region:
<div class="titlebar"> <div class="title">My Application</div> <div class="window-controls"> <button class="window-button minimize" aria-label="Minimize"></button> <button class="window-button maximize" aria-label="Maximize"></button> <button class="window-button close" aria-label="Close"></button> </div></div>.titlebar { --wails-non-client-region: caption; height: 40px;}
.window-controls { display: flex; height: 100%;}
.window-button { width: 46px; border: 0; background: transparent;}
.window-button.minimize { --wails-non-client-region: minimize;}
.window-button.maximize { --wails-non-client-region: maximize;}
.window-button.close { --wails-non-client-region: close;}Supported --wails-non-client-region values:
caption - draggable caption areaminimize - native minimize button hit targetmaximize - native maximize button hit target, including Windows 11 Snap Assist / Snap Layouts hover behaviorclose - native close button hit targetThe Wails runtime observes DOM, style, size, scroll, and viewport changes, then sends region snapshots to the native window. Region geometry is measured in CSS pixels and converted to physical pixels for Windows hit testing.
The visual design remains entirely yours. The regions only tell Windows what each rectangle means; the button shape, icon, color, spacing, hover styling, and layout still come from your frontend.
You can enable both options when you want WebView2 app-region support and Wails-managed caption button regions in the same window:
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Frameless: true, Windows: application.WindowsWindow{ NonClientRegionSupport: true, WebView2CompositionHosting: true, },})Go side:
type WindowControls struct { window *application.WebviewWindow}
func (wc *WindowControls) Minimise() { wc.window.Minimise()}
func (wc *WindowControls) Maximise() { if wc.window.IsMaximised() { wc.window.UnMaximise() } else { wc.window.Maximise() }}
func (wc *WindowControls) Close() { wc.window.Close()}JavaScript side:
import { Minimise, Maximise, Close } from './bindings/WindowControls'
document.querySelector('.minimize').addEventListener('click', Minimise)document.querySelector('.maximize').addEventListener('click', Maximise)document.querySelector('.close').addEventListener('click', Close)Or use runtime methods:
import { Window } from '@wailsio/runtime'
document.querySelector('.minimize').addEventListener('click', () => Window.Minimise())document.querySelector('.maximize').addEventListener('click', () => Window.Maximise())document.querySelector('.close').addEventListener('click', () => Window.Close())Track maximise state for button icon:
import { Window } from '@wailsio/runtime'
async function toggleMaximise() { const isMaximised = await Window.IsMaximised()
if (isMaximised) { await Window.Restore() } else { await Window.Maximise() }
updateMaximiseButton()}
async function updateMaximiseButton() { const isMaximised = await Window.IsMaximised() const button = document.querySelector('.maximize') button.textContent = isMaximised ? '❐' : '□'}Wails provides automatic resize handles for frameless windows:
/* Enable resize on all edges */body { --wails-resize: all;}
/* Or specific edges */.resize-top { --wails-resize: top;}
.resize-bottom { --wails-resize: bottom;}
.resize-left { --wails-resize: left;}
.resize-right { --wails-resize: right;}
/* Corners */.resize-top-left { --wails-resize: top-left;}
.resize-top-right { --wails-resize: top-right;}
.resize-bottom-left { --wails-resize: bottom-left;}
.resize-bottom-right { --wails-resize: bottom-right;}Values:
all - Resize from all edgestop, bottom, left, right - Specific edgestop-left, top-right, bottom-left, bottom-right - Cornersnone - No resize<div class="window"> <div class="titlebar">...</div> <div class="content">...</div> <div class="resize-handle resize-bottom-right"></div></div>.resize-handle { position: absolute; width: 16px; height: 16px;}
.resize-bottom-right { --wails-resize: bottom-right; bottom: 0; right: 0; cursor: nwse-resize;}Windows frameless windows:
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Frameless: true, Windows: application.WindowsWindow{ DisableFramelessWindowDecorations: false, },})Features:
Disable decorations:
Windows: application.WindowsWindow{ DisableFramelessWindowDecorations: true,},Snap Assist:
// Trigger Windows 11 Snap Assistwindow.SnapAssist()This triggers Snap Layouts through the Windows hotkey path. For a custom HTML maximize button with native hover Snap Layouts, use Native Non-Client Regions on Windows instead.
Custom title bar height: Windows automatically detects drag regions from CSS.
macOS frameless windows:
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Frameless: true, Mac: application.MacWindow{ TitleBar: application.MacTitleBar{ AppearsTransparent: true, }, InvisibleTitleBarHeight: 40, },})Features:
Hide the title bar entirely (use the preset variants exported from the application package — there is no TitleBarStyle field or MacTitleBarStyleHidden constant):
Mac: application.MacWindow{ TitleBar: application.MacTitleBarHidden,},Other presets include MacTitleBarDefault, MacTitleBarHiddenInset, and MacTitleBarHiddenInsetUnified.
Invisible title bar:
Allows dragging whilst hiding the title bar. This only takes effect when the window is frameless or uses AppearsTransparent:
Mac: application.MacWindow{ InvisibleTitleBarHeight: 40,},Linux frameless windows:
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Frameless: true,})Features:
Desktop environment notes:
Compositor required: Transparency requires a compositor (most modern DEs have one).
<div class="modern-titlebar"> <div class="app-icon"> <img src="/icon.png" alt="App Icon"> </div> <div class="title">My Application</div> <div class="controls"> <button class="minimize">−</button> <button class="maximize">□</button> <button class="close">×</button> </div></div>.modern-titlebar { --wails-draggable: drag; display: flex; align-items: center; height: 40px; background: linear-gradient(to bottom, #3a3a3a, #2c2c2c); border-bottom: 1px solid #1a1a1a; padding: 0 16px;}
.app-icon { --wails-draggable: no-drag; width: 24px; height: 24px; margin-right: 12px;}
.title { flex: 1; font-size: 13px; color: #e0e0e0; user-select: none;}
.controls { display: flex; gap: 1px;}
.controls button { --wails-draggable: no-drag; width: 46px; height: 32px; border: none; background: transparent; color: #e0e0e0; font-size: 14px; cursor: pointer; transition: background 0.2s;}
.controls button:hover { background: rgba(255, 255, 255, 0.1);}
.controls .close:hover { background: #e81123; color: white;}splash := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Loading...", Width: 400, Height: 300, Frameless: true, AlwaysOnTop: true, BackgroundType: application.BackgroundTypeTransparent,})body { background: transparent; display: flex; justify-content: center; align-items: center;}
.splash { background: white; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); padding: 40px; text-align: center;}window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Frameless: true, BackgroundType: application.BackgroundTypeTransparent,})body { background: transparent; margin: 8px;}
.window { background: white; border-radius: 16px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15); overflow: hidden; height: calc(100vh - 16px);}
.titlebar { --wails-draggable: drag; background: #f5f5f5; border-bottom: 1px solid #e0e0e0;}overlay := app.Window.NewWithOptions(application.WebviewWindowOptions{ Frameless: true, AlwaysOnTop: true, BackgroundType: application.BackgroundTypeTransparent,})body { background: transparent;}
.overlay { background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(10px); border-radius: 8px; padding: 20px;}Here’s a production-ready frameless window:
Go:
package main
import ( _ "embed" "github.com/wailsapp/wails/v3/pkg/application")
//go:embed frontend/distvar assets embed.FS
func main() { app := application.New(application.Options{ Name: "Frameless App", Assets: application.AssetOptions{ Handler: application.AssetFileServerFS(assets), }, })
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Frameless Application", Width: 1000, Height: 700, MinWidth: 800, MinHeight: 600, Frameless: true,
Mac: application.MacWindow{ TitleBar: application.MacTitleBar{ AppearsTransparent: true, }, InvisibleTitleBarHeight: 40, },
Windows: application.WindowsWindow{ DisableFramelessWindowDecorations: false, }, })
window.Center() window.Show()
app.Run()}HTML:
<!DOCTYPE html><html><head> <link rel="stylesheet" href="/style.css"></head><body> <div class="window"> <div class="titlebar"> <div class="title">Frameless Application</div> <div class="controls"> <button class="minimize" title="Minimise">−</button> <button class="maximize" title="Maximise">□</button> <button class="close" title="Close">×</button> </div> </div> <div class="content"> <h1>Hello from Frameless Window!</h1> </div> </div> <script src="/main.js" type="module"></script></body></html>CSS:
* { margin: 0; padding: 0; box-sizing: border-box;}
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5;}
.window { height: 100vh; display: flex; flex-direction: column;}
.titlebar { --wails-draggable: drag; display: flex; justify-content: space-between; align-items: center; height: 40px; background: #ffffff; border-bottom: 1px solid #e0e0e0; padding: 0 16px;}
.title { font-size: 13px; font-weight: 500; color: #333; user-select: none;}
.controls { display: flex; gap: 8px;}
.controls button { --wails-draggable: no-drag; width: 32px; height: 32px; border: none; background: transparent; color: #666; font-size: 16px; cursor: pointer; border-radius: 4px; display: flex; align-items: center; justify-content: center; transition: all 0.2s;}
.controls button:hover { background: #f0f0f0; color: #333;}
.controls .close:hover { background: #e81123; color: white;}
.content { flex: 1; padding: 40px; overflow: auto;}JavaScript:
import { Window } from '@wailsio/runtime'
// Minimise buttondocument.querySelector('.minimize').addEventListener('click', () => { Window.Minimise()})
// Maximise/restore buttonconst maximiseBtn = document.querySelector('.maximize')maximiseBtn.addEventListener('click', async () => { const isMaximised = await Window.IsMaximised()
if (isMaximised) { await Window.Restore() } else { await Window.Maximise() }
updateMaximiseButton()})
// Close buttondocument.querySelector('.close').addEventListener('click', () => { Window.Close()})
// Update maximise button iconasync function updateMaximiseButton() { const isMaximised = await Window.IsMaximised() maximiseBtn.textContent = isMaximised ? '❐' : '□' maximiseBtn.title = isMaximised ? 'Restore' : 'Maximise'}
// Initial stateupdateMaximiseButton()Cause: Missing --wails-draggable: drag
Solution:
.titlebar { --wails-draggable: drag;}Cause: Buttons are in draggable area
Solution:
.titlebar button { --wails-draggable: no-drag;}Cause: Missing resize handles
Solution:
body { --wails-resize: all;}Window Basics
Learn the fundamentals of window management.
Window Options
Complete reference for window options.
Window Events
Handle window lifecycle events.
Multiple Windows
Patterns for multi-window applications.
Questions? Ask in Discord or check the frameless example.