Frameless Windows
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.

The example above is the default Wails v3 TypeScript starter app with Frameless: true enabled.
Quick Start
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.
Creating Frameless Windows
Corner Radius (macOS)
Frameless windows preserve AppKit’s standard rounded macOS corners by default. Set Mac.CornerRadius to use a custom radius (in points):
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
Mac: application.MacWindow{
CornerRadius: 16,
},
})Set Mac.CornerType to MacWindowCornerTypeSquare for square corners. This ignores CornerRadius:
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
Mac: application.MacWindow{
CornerType: application.MacWindowCornerTypeSquare,
},
})Basic Frameless Window
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)
With Transparent Background
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
BackgroundType: application.BackgroundTypeTransparent,
})Use cases:
- Rounded corners
- Custom shapes
- Overlay windows
- Splash screens
Drag Regions
CSS-Based Dragging
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)
Complete Title Bar Example
<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())Native Non-Client Regions on Windows
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-regionthrough WebView2’s native non-client region support--wails-non-client-regionthrough Wails runtime tracking for custom caption buttons
Choosing a Mode
Choose based on what you need from Windows:
- Use
NonClientRegionSupportfor simple native app dragging with WebView2’sapp-region: dragandapp-region: no-drag. - Use
WebView2CompositionHostingwhen your custom minimize, maximize, and close buttons should behave like native Windows caption buttons. - Enable both when the same window needs WebView2-native
app-regionsupport 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.
WebView2 app-region
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
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 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.
Combining Both
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,
},
})System Buttons
Implementing Close/Minimise/Maximise
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())Toggle Maximise State
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 ? '❐' : '□'
}Resize Handles
CSS-Based Resize
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
Resize Handle Example
<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;
}Platform-Specific Behaviour
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.
macOS frameless windows:
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Frameless: true,
Mac: application.MacWindow{
TitleBar: application.MacTitleBar{
AppearsTransparent: true,
},
InvisibleTitleBarHeight: 40,
},
})Features:
- Native fullscreen support
- Traffic light buttons (optional)
- Vibrancy effects
- Transparent title bar
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:
- Basic frameless support
- CSS drag regions
- Varies by desktop environment
Desktop environment notes:
- GNOME: Good support
- KDE Plasma: Good support
- XFCE: Basic support
- Tiling WMs: Limited support
Compositor required: Transparency requires a compositor (most modern DEs have one).
Common Patterns
Pattern 1: Modern Title Bar
<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;
}Pattern 2: Splash Screen
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;
}Pattern 3: Rounded Window
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;
}Pattern 4: Overlay Window
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;
}Complete Example
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()Best Practices
✅ Do
- 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
- 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
Troubleshooting
Window Won’t Drag
Cause: Missing --wails-draggable: drag
Solution:
.titlebar {
--wails-draggable: drag;
}Buttons Don’t Work
Cause: Buttons are in draggable area
Solution:
.titlebar button {
--wails-draggable: no-drag;
}Can’t Resize Window
Cause: Missing resize handles
Solution:
body {
--wails-resize: all;
}Next Steps
Learn the fundamentals of window management.
Complete reference for window options.
Handle window lifecycle events.
Patterns for multi-window applications.
Questions? Ask in Discord or check the frameless example.