Custom titlebar

--wails-draggable + Tailwind.

7 minmedium

A frameless Wails window has no OS titlebar — you draw your own. Pros: it looks like Linear / Discord / VS Code, not Win32. Cons: you have to wire drag-to-move yourself. This lesson covers both.

Enable frameless in main.go

main.go (excerpt)
wails.Run(&options.App{
Title: "Field POS",
Width: 1280,
Height: 800,
Frameless: true, // ← removes OS titlebar
CSSDragProperty: "--wails-draggable",
CSSDragValue: "drag",
// ...
})

Frameless + the CSS drag declarations let you mark which HTML elements can drag the window. Anything with style="--wails-draggable: drag" becomes a drag handle.

The titlebar component

src/components/titlebar.tsx
import { Quit, WindowMinimise, WindowToggleMaximise } from '../../wailsjs/runtime/runtime'
import { Minus, Square, X } from 'lucide-react'
export function TitleBar() {
return (
<div
style={{ '--wails-draggable': 'drag' } as any}
className="h-10 flex items-center justify-between px-4 bg-card border-b border-border select-none"
>
{/* Left: app icon + title */}
<div className="flex items-center gap-2">
<img src="/icon.svg" className="h-5 w-5" alt="" />
<span className="text-sm font-medium">Field POS</span>
</div>
{/* Right: window controls — NOT draggable */}
<div
style={{ '--wails-draggable': 'no-drag' } as any}
className="flex items-center"
>
<WinBtn onClick={WindowMinimise}><Minus className="h-4 w-4" /></WinBtn>
<WinBtn onClick={WindowToggleMaximise}><Square className="h-3 w-3" /></WinBtn>
<WinBtn onClick={Quit} danger><X className="h-4 w-4" /></WinBtn>
</div>
</div>
)
}
function WinBtn({ onClick, danger, children }) {
return (
<button
onClick={onClick}
className={`h-10 w-12 flex items-center justify-center hover:bg-muted ${danger ? 'hover:bg-red-500 hover:text-white' : ''}`}
>
{children}
</button>
)
}

Two key bits:

  • The outer div has --wails-draggable: drag — the whole titlebar can drag the window.
  • The window-controls cluster has --wails-draggable: no-drag — buttons are clickable, not drag handles.
Always mark buttons as no-drag. Without it, clicking the X starts a drag-then-click, the user's click registers on whatever happens to be under the cursor after the drag, and the X doesn't fire. Annoying for the user; subtle for you.

The Wails runtime API

  • WindowMinimise() — minimise to taskbar
  • WindowToggleMaximise() — toggle full vs. windowed
  • WindowFullscreen() — true OS fullscreen (different from maximise)
  • Quit() — exit the app
  • WindowSetTitle(s) — change the title at runtime
  • WindowCenter() — recenter on screen

All exported from ../wailsjs/runtime/runtime and ready to import.

Why frameless is the right default for line-of-business apps

  • Brand consistency. Your app looks the same on every OS. No mismatched native-vs-web fonts in the titlebar.
  • More vertical pixels. No 30px OS titlebar eating space. For dashboards, every pixel counts.
  • Custom controls. Add a tab bar, search box, or status indicator IN the titlebar. Common pattern in spreadsheet-shaped apps.

Quick check

A user reports 'the close button doesn't work when I click it'. You inspect — `Quit` is wired correctly. What's the bug?

Try it

Build a working titlebar:

  1. Set Frameless: true + the CSS drag declarations in main.go.
  2. Add the TitleBar component above your main layout.
  3. Drag the window by the empty area — confirm it moves.
  4. Click each window button — Minimise, Maximise, Close — all should work.

What's next

Last lesson of the chapter — going deeper on the window controls: traffic-light styles on Mac, double-click-to-maximize, and the edge cases.

Spot a typo? Have an idea?

Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.

Suggest an improvement on GitHub