Window Events
Window Events
Wails dispatches window lifecycle and state-change events through a single API: OnWindowEvent for passive listeners, and RegisterHook for cancellable hooks that can prevent the default action.
import "github.com/wailsapp/wails/v3/pkg/events"
// Listener — observes the event, cannot cancel it.
window.OnWindowEvent(events.Common.WindowDidResize, func(e *application.WindowEvent) { /* ... */ })
// Hook — runs before listeners; can call e.Cancel() to suppress the default action.
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
if hasUnsavedChanges() {
e.Cancel() // prevent the window from closing
}
})Both calls return an unsubscribe func() you can call to remove the handler.
Cross-platform events live in events.Common.*. Platform-specific events live in events.Mac.*, events.Windows.*, and events.Linux.*. The full list is generated in v3/pkg/events/events.go.
Lifecycle Events
Window Creation
Run a callback every time a window is created using app.Window.OnCreate:
app.Window.OnCreate(func(window application.Window) {
fmt.Printf("Window created: %s (ID: %d)\n", window.Name(), window.ID())
// Configure all new windows
window.SetMinSize(400, 300)
// Register a hook for this window
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
if !confirmClose() {
e.Cancel()
}
})
})The callback receives application.Window (an interface). The OnCreate callback is invoked once per window, after the window’s runtime is initialised.
WindowClosing
Fires when the user attempts to close the window (clicking X, ⌘W, Alt+F4, etc.).
Use a hook (RegisterHook) — only hooks can cancel the close. Listeners observe the event but cannot prevent it.
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
if hasUnsavedChanges() {
// Cancel the close — the window stays open.
e.Cancel()
}
})Important:
WindowClosingis dispatched on user-initiated close attempts.- A
RegisterHookcallback can calle.Cancel()to keep the window open. OnWindowEventcallbacks forWindowClosingare observers — they fire but cannot cancel.
Programmatic close: call window.Close(). There is no window.Destroy().
WindowRuntimeReady
Fires once the in-window runtime has finished initialising — safe point to call into the window’s JS context:
window.OnWindowEvent(events.Common.WindowRuntimeReady, func(e *application.WindowEvent) {
window.EmitEvent("app-ready", nil)
})Focus Events
WindowFocus
Called when the window gains focus:
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
fmt.Println("Window gained focus")
updateTitleBar(true)
app.Event.Emit("window-focused", window.ID())
})WindowLostFocus
Called when the window loses focus:
window.OnWindowEvent(events.Common.WindowLostFocus, func(e *application.WindowEvent) {
fmt.Println("Window lost focus")
updateTitleBar(false)
saveCurrentState()
})Example: Focus-aware UI:
type FocusAwareWindow struct {
window *application.WebviewWindow
focused bool
}
func (fw *FocusAwareWindow) Setup() {
fw.window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
fw.focused = true
fw.updateAppearance()
})
fw.window.OnWindowEvent(events.Common.WindowLostFocus, func(e *application.WindowEvent) {
fw.focused = false
fw.updateAppearance()
})
}
func (fw *FocusAwareWindow) updateAppearance() {
if fw.focused {
fw.window.EmitEvent("update-theme", "active")
} else {
fw.window.EmitEvent("update-theme", "inactive")
}
}State Change Events
WindowMinimise / WindowUnMinimise
window.OnWindowEvent(events.Common.WindowMinimise, func(e *application.WindowEvent) {
pauseRendering()
saveWindowState()
})
window.OnWindowEvent(events.Common.WindowUnMinimise, func(e *application.WindowEvent) {
resumeRendering()
refreshContent()
})WindowMaximise / WindowUnMaximise
window.OnWindowEvent(events.Common.WindowMaximise, func(e *application.WindowEvent) {
window.EmitEvent("layout-mode", "maximised")
})
window.OnWindowEvent(events.Common.WindowUnMaximise, func(e *application.WindowEvent) {
window.EmitEvent("layout-mode", "normal")
})WindowFullscreen / WindowUnFullscreen
window.OnWindowEvent(events.Common.WindowFullscreen, func(e *application.WindowEvent) {
window.EmitEvent("chrome-visibility", false)
window.EmitEvent("layout-mode", "fullscreen")
})
window.OnWindowEvent(events.Common.WindowUnFullscreen, func(e *application.WindowEvent) {
window.EmitEvent("chrome-visibility", true)
window.EmitEvent("layout-mode", "normal")
})Enter/exit fullscreen programmatically with window.Fullscreen() / window.UnFullscreen() / window.ToggleFullscreen() — there is no SetFullscreen(bool). Query state with window.IsFullscreen() bool.
Position and Size Events
WindowDidMove
window.OnWindowEvent(events.Common.WindowDidMove, func(e *application.WindowEvent) {
x, y := window.Position()
fmt.Printf("Window moved to: %d, %d\n", x, y)
saveWindowPosition(x, y)
})WindowDidResize
window.OnWindowEvent(events.Common.WindowDidResize, func(e *application.WindowEvent) {
width, height := window.Size()
fmt.Printf("Window resized to: %dx%d\n", width, height)
saveWindowSize(width, height)
window.EmitEvent("window-size", map[string]int{
"width": width,
"height": height,
})
})WindowDidResize and WindowDidMove do not carry coordinates on the event itself — query the window via window.Size() / window.Position() inside the callback.
Example: Responsive layout:
window.OnWindowEvent(events.Common.WindowDidResize, func(e *application.WindowEvent) {
width, _ := window.Size()
var layout string
switch {
case width < 600:
layout = "compact"
case width < 1200:
layout = "normal"
default:
layout = "wide"
}
window.EmitEvent("layout-changed", layout)
})Complete Example
A production-ready window with full event handling:
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
type WindowState struct {
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
Maximised bool `json:"maximised"`
Fullscreen bool `json:"fullscreen"`
}
type ManagedWindow struct {
app *application.App
window *application.WebviewWindow
state WindowState
dirty bool
}
func main() {
app := application.New(application.Options{
Name: "Event Demo",
})
mw := &ManagedWindow{app: app}
mw.CreateWindow()
mw.LoadState()
mw.SetupEventHandlers()
app.Run()
}
func (mw *ManagedWindow) CreateWindow() {
mw.window = mw.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Event Demo",
Width: 800,
Height: 600,
})
}
func (mw *ManagedWindow) SetupEventHandlers() {
// Focus events
mw.window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
mw.window.EmitEvent("focus-state", true)
})
mw.window.OnWindowEvent(events.Common.WindowLostFocus, func(e *application.WindowEvent) {
mw.window.EmitEvent("focus-state", false)
})
// State change events
mw.window.OnWindowEvent(events.Common.WindowMinimise, func(e *application.WindowEvent) {
mw.SaveState()
})
mw.window.OnWindowEvent(events.Common.WindowMaximise, func(e *application.WindowEvent) {
mw.state.Maximised = true
mw.dirty = true
})
mw.window.OnWindowEvent(events.Common.WindowUnMaximise, func(e *application.WindowEvent) {
mw.state.Maximised = false
mw.dirty = true
})
mw.window.OnWindowEvent(events.Common.WindowFullscreen, func(e *application.WindowEvent) {
mw.state.Fullscreen = true
mw.dirty = true
})
mw.window.OnWindowEvent(events.Common.WindowUnFullscreen, func(e *application.WindowEvent) {
mw.state.Fullscreen = false
mw.dirty = true
})
// Position and size events
mw.window.OnWindowEvent(events.Common.WindowDidMove, func(e *application.WindowEvent) {
mw.state.X, mw.state.Y = mw.window.Position()
mw.dirty = true
})
mw.window.OnWindowEvent(events.Common.WindowDidResize, func(e *application.WindowEvent) {
mw.state.Width, mw.state.Height = mw.window.Size()
mw.dirty = true
})
// Cancellable close — use a hook, not a listener.
mw.window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
if mw.dirty {
mw.SaveState()
}
})
}
func (mw *ManagedWindow) LoadState() {
data, err := os.ReadFile("window-state.json")
if err != nil {
return
}
if err := json.Unmarshal(data, &mw.state); err != nil {
return
}
// Restore window state
mw.window.SetPosition(mw.state.X, mw.state.Y)
mw.window.SetSize(mw.state.Width, mw.state.Height)
if mw.state.Maximised {
mw.window.Maximise()
}
if mw.state.Fullscreen {
mw.window.Fullscreen()
}
}
func (mw *ManagedWindow) SaveState() {
data, err := json.Marshal(mw.state)
if err != nil {
return
}
os.WriteFile("window-state.json", data, 0644)
mw.dirty = false
fmt.Println("Window state saved")
}Event Coordination
Cross-Window Events
Coordinate between multiple windows using the application event bus:
// In main window
mainWindow.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
app.Event.Emit("main-window-focused", nil)
})
// In other windows
app.Event.On("main-window-focused", func(event *application.CustomEvent) {
updateRelativeToMain()
})Event Chains
window.OnWindowEvent(events.Common.WindowMaximise, func(e *application.WindowEvent) {
saveWindowState()
window.EmitEvent("layout-changed", "maximised")
app.Event.Emit("window-maximised", window.ID())
})Debounced Events
var resizeTimer *time.Timer
window.OnWindowEvent(events.Common.WindowDidResize, func(e *application.WindowEvent) {
if resizeTimer != nil {
resizeTimer.Stop()
}
resizeTimer = time.AfterFunc(500*time.Millisecond, func() {
w, h := window.Size()
saveWindowSize(w, h)
})
})Best Practices
✅ Do
- Use hooks for cancellation — only
RegisterHookcallbacks can calle.Cancel(). - Save state on close — restore window position/size next launch.
- Debounce frequent events —
WindowDidResizeandWindowDidMovefire rapidly. - Handle focus changes — update UI appropriately.
- Coordinate via events —
app.Event.Emitfor cross-window messaging. - Unsubscribe when handlers are no longer needed —
OnWindowEvent/RegisterHookboth return an unsubscribefunc().
❌ Don’t
- Don’t block event handlers — keep them fast.
- Don’t try to cancel from
OnWindowEvent— useRegisterHook. - Don’t use
window.Destroy()— it doesn’t exist; usewindow.Close(). - Don’t save on every event — debounce first.
- Don’t assume coordinates on the event — call
window.Position()/window.Size().
Troubleshooting
WindowClosing Hook Doesn’t Prevent Close
Cause: You used OnWindowEvent instead of RegisterHook.
Solution: Only RegisterHook callbacks can call e.Cancel(). OnWindowEvent callbacks observe; they cannot cancel.
// ❌ Cannot cancel — this is a listener.
window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) {
e.Cancel() // no effect
})
// ✅ Can cancel — this is a hook.
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
e.Cancel()
})Events Not Firing
Cause: Handler registered after the event occurred.
Solution: Register handlers immediately after window creation (or in the app.Window.OnCreate callback).
window := app.Window.New()
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) { /* ... */ })Memory Leaks
Cause: Long-lived handlers held by short-lived state.
Solution: Keep the unsubscribe func() returned by OnWindowEvent/RegisterHook and call it during cleanup.
unsub := window.OnWindowEvent(events.Common.WindowDidResize, handler)
// ...later:
unsub()Next Steps
Window Basics - Learn the fundamentals of window management Learn More →
Multiple Windows - Patterns for multi-window applications Learn More →
Events System - Deep dive into the event system Learn More →
Application Lifecycle - Understand the application lifecycle Learn More →