Skip to content

Frameless Windows

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:

  • No title bar
  • No window borders
  • No system buttons
  • Transparent background (optional)

What you need to implement:

  • Draggable area
  • Close/minimise/maximise buttons
  • Resize handles (if resizable)
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
BackgroundType: application.BackgroundTypeTransparent,
})

Use cases:

  • Rounded corners
  • Custom shapes
  • Overlay windows
  • Splash screens

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 draggable
  • no-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 buttons

Choose based on what you need from Windows:

  • Use NonClientRegionSupport for simple native app dragging with WebView2’s app-region: drag and app-region: no-drag.
  • Use WebView2CompositionHosting when your custom minimize, maximize, and close buttons should behave like native Windows caption buttons.
  • Enable both when the same window needs WebView2-native 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.

Custom Caption Buttons with Native Behavior

Section titled “Custom Caption Buttons with Native Behavior”

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 area
  • minimize - native minimize button hit target
  • maximize - native maximize button hit target, including Windows 11 Snap Assist / Snap Layouts hover behavior
  • close - native close button hit target

The 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 edges
  • top, bottom, left, right - Specific edges
  • top-left, top-right, bottom-left, bottom-right - Corners
  • none - 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:

  • Automatic drop shadow
  • Snap layouts support (Windows 11)
  • Aero Snap support
  • DPI scaling

Disable decorations:

Windows: application.WindowsWindow{
DisableFramelessWindowDecorations: true,
},

Snap Assist:

// Trigger Windows 11 Snap Assist
window.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.

<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/dist
var 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 button
document.querySelector('.minimize').addEventListener('click', () => {
Window.Minimise()
})
// Maximise/restore button
const maximiseBtn = document.querySelector('.maximize')
maximiseBtn.addEventListener('click', async () => {
const isMaximised = await Window.IsMaximised()
if (isMaximised) {
await Window.Restore()
} else {
await Window.Maximise()
}
updateMaximiseButton()
})
// Close button
document.querySelector('.close').addEventListener('click', () => {
Window.Close()
})
// Update maximise button icon
async function updateMaximiseButton() {
const isMaximised = await Window.IsMaximised()
maximiseBtn.textContent = isMaximised ? '' : ''
maximiseBtn.title = isMaximised ? 'Restore' : 'Maximise'
}
// Initial state
updateMaximiseButton()
  • Provide draggable area - Users need to move the window
  • Implement system buttons - Close, minimise, maximise
  • Set minimum size - Prevent unusable layouts
  • Test on all platforms - Behaviour varies
  • Use CSS for drag regions - Flexible and maintainable
  • Provide visual feedback - Hover states on buttons
  • Don’t forget resize handles - If window is resizable
  • Don’t make entire window draggable - Prevents interaction
  • Don’t forget no-drag on buttons - They won’t work
  • Don’t use tiny drag areas - Hard to grab
  • Don’t forget platform differences - Test thoroughly

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.

Learn More →

Multiple Windows

Patterns for multi-window applications.

Learn More →


Questions? Ask in Discord or check the frameless example.