Window controls

Min, max, close, traffic lights.

6 minmedium

Polishing the window controls — OS-conventional layouts, the unmaximize state, double-click-to-maximize, the edge cases users will report.

Windows vs. macOS controls — different conventions

  • Windows: Minimise / Maximise / Close on the RIGHT, in that order. Square boxes, ~12-14px icons.
  • macOS: Close / Minimise / Zoom on the LEFT, colored traffic lights (red / yellow / green).

Wails ships the native OS traffic lights on macOS if options.Mac isn't set to hide them. The simplest approach: use Wails's defaults on Mac, draw your own on Windows.

OS detection in React

src/lib/os.ts
import { Environment } from '../../wailsjs/runtime/runtime'
let cachedOS: 'windows' | 'darwin' | 'linux' | null = null
export async function detectOS() {
if (cachedOS) return cachedOS
const env = await Environment()
cachedOS = env.platform as any
return cachedOS
}

Now your titlebar can render OS-appropriate controls or hide them entirely on Mac (Wails handles the traffic lights).

Double-click to maximise

Standard convention — double-click the titlebar maximises / unmaximises. Wire it:

<div
style={{ '--wails-draggable': 'drag' } as any}
onDoubleClick={WindowToggleMaximise}
className="h-10 flex items-center justify-between px-4 ..."
>
...

One line. Now power users have the shortcut they expect.

Tracking maximised state for the icon swap

Convention: the maximise button shows a single square when the window is normal, two stacked squares (or a restore icon) when maximised. Listen to the Wails event:

import { EventsOn } from '../../wailsjs/runtime/runtime'
export function useIsMaximised() {
const [max, setMax] = useState(false)
useEffect(() => {
const cleanup = EventsOn('wails:window:maximise', () => setMax(true))
const cleanup2 = EventsOn('wails:window:unmaximise', () => setMax(false))
return () => { cleanup(); cleanup2() }
}, [])
return max
}
const isMax = useIsMaximised()
<WinBtn onClick={WindowToggleMaximise}>
{isMax ? <Restore /> : <Square />}
</WinBtn>
Quit vs. Close. Wails' Quit() ends the process. Some apps want close-to-tray instead (Slack, Discord — clicking X hides the window but the process stays). Wire that with the OnBeforeClose hook in main.go.

Keyboard shortcuts

  • Cmd/Ctrl+W — close window. Wire via Wails accelerators or with an HTML keyboard listener.
  • Cmd/Ctrl+M — minimise. macOS does this automatically; Wails has it.
  • F11 — fullscreen toggle. Wire to WindowFullscreen().

Edge cases users WILL report

  • Window remembers wrong size on relaunch. Save the geometry on close, restore on startup. Wails has WindowGetSize / WindowSetPosition for this.
  • App restored on a disconnected monitor. Detect off-screen position; reset to centre.
  • HiDPI / scale 200%. Test on a high-DPI display. Pixel measurements that look fine at 100% can blur at 200%.

Quick check

A user wants the app to close to the system tray (like Slack), not quit. Where do you wire that?

Try it

For chapter 3's assignment, polish the titlebar to production quality:

  1. Add the OS detection — show OS-appropriate controls.
  2. Add double-click-to-maximize.
  3. Swap the maximize icon between single-square and restore-icon based on state.
  4. Save + restore the window size on close + relaunch.
  5. Test on at least one OS other than your dev machine (Hyper-V Windows VM if you're on Mac, OrbStack macOS if you're on Linux).

What's next

Chapter 4 — In-app auto-update. The most impactful feature for a shipped desktop app. Bug fixes land without users having to re-download installers.

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