Custom Events
Create your own event types.
Wails provides a unified event system for pub/sub communication. Emit events from anywhere, listen from anywhere—Go to JavaScript, JavaScript to Go, window to window—enabling decoupled architecture with typed events and lifecycle hooks.
Go (emit):
app.Event.Emit("user-logged-in", map[string]interface{}{ "userId": 123, "name": "Alice",})JavaScript (listen):
import { Events } from '@wailsio/runtime'
Events.On("user-logged-in", (event) => { console.log(`User ${event.data.name} logged in`)})That’s it! Cross-language pub/sub.
Your application-specific events:
// Emit from Goapp.Event.Emit("order-created", order)app.Event.Emit("payment-processed", payment)app.Event.Emit("notification", message)// Listen in JavaScriptEvents.On("order-created", handleOrder)Events.On("payment-processed", handlePayment)Events.On("notification", showNotification)Built-in OS and application events:
import "github.com/wailsapp/wails/v3/pkg/events"
// Theme changesapp.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) { if e.Context().IsDarkMode() { app.Logger.Info("Dark mode enabled") }})
// Application lifecycleapp.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) { app.Logger.Info("Application started")})Window-specific events:
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) { app.Logger.Info("Window focused")})
window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) { app.Logger.Info("Window closing")})Basic emit:
app.Event.Emit("event-name", data)With different data types:
// Stringapp.Event.Emit("message", "Hello")
// Numberapp.Event.Emit("count", 42)
// Structapp.Event.Emit("user", User{ID: 1, Name: "Alice"})
// Mapapp.Event.Emit("config", map[string]interface{}{ "theme": "dark", "fontSize": 14,})
// Arrayapp.Event.Emit("items", []string{"a", "b", "c"})To specific window:
window.EmitEvent("window-specific-event", data)import { Events } from '@wailsio/runtime'
// Emit to GoEvents.Emit("button-clicked", { buttonId: "submit" })
// Emit to all windowsEvents.Emit("broadcast-message", "Hello everyone")Application events:
app.Event.On("custom-event", func(e *application.CustomEvent) { data := e.Data // Handle event})With type assertion:
app.Event.On("user-updated", func(e *application.CustomEvent) { user := e.Data.(User) app.Logger.Info("User updated", "name", user.Name)})Multiple handlers:
// All handlers will be calledapp.Event.On("order-created", logOrder)app.Event.On("order-created", sendEmail)app.Event.On("order-created", updateInventory)Basic listener:
import { Events } from '@wailsio/runtime'
Events.On("event-name", (event) => { console.log("Event received:", event.data)})With cleanup:
const unsubscribe = Events.On("event-name", handleEvent)
// Later, stop listeningunsubscribe()Multiple handlers:
Events.On("data-updated", updateUI)Events.On("data-updated", saveToCache)Events.On("data-updated", logChange)One Time handlers:
Events.Once("data-updated", updateVariable)Common events (cross-platform):
import "github.com/wailsapp/wails/v3/pkg/events"
// Application startedapp.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) { app.Logger.Info("App started")})
// Theme changedapp.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) { isDark := e.Context().IsDarkMode() app.Event.Emit("theme-changed", isDark)})
// File openedapp.Event.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *application.ApplicationEvent) { filePath := e.Context().OpenedFile() openFile(filePath)})Platform-specific events:
// Application became activeapp.Event.OnApplicationEvent(events.Mac.ApplicationDidBecomeActive, func(e *application.ApplicationEvent) { app.Logger.Info("App became active")})
// Application will terminateapp.Event.OnApplicationEvent(events.Mac.ApplicationWillTerminate, func(e *application.ApplicationEvent) { cleanup()})// Power status changedapp.Event.OnApplicationEvent(events.Windows.APMPowerStatusChange, func(e *application.ApplicationEvent) { app.Logger.Info("Power status changed")})
// System suspendingapp.Event.OnApplicationEvent(events.Windows.APMSuspend, func(e *application.ApplicationEvent) { saveState()})// Application startupapp.Event.OnApplicationEvent(events.Linux.ApplicationStartup, func(e *application.ApplicationEvent) { app.Logger.Info("App starting")})
// Theme changedapp.Event.OnApplicationEvent(events.Linux.SystemThemeChanged, func(e *application.ApplicationEvent) { updateTheme()})Common window events:
// Window focuswindow.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) { app.Logger.Info("Window focused")})
// Window blurwindow.OnWindowEvent(events.Common.WindowBlur, func(e *application.WindowEvent) { app.Logger.Info("Window blurred")})
// Window closingwindow.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) { if hasUnsavedChanges() { e.Cancel() // Prevent close }})
// Window closedwindow.OnWindowEvent(events.Common.WindowClosed, func(e *application.WindowEvent) { cleanup()})Hooks run before standard listeners and can cancel events:
// Hook - runs first, can cancelwindow.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { if hasUnsavedChanges() { result := showConfirmdialog("Unsaved changes. Close anyway?") if result != "yes" { e.Cancel() // Prevent window close } }})
// Standard listener - runs after hookswindow.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) { app.Logger.Info("Window closing")})Key differences:
| Feature | Hooks | Standard Listeners |
|---|---|---|
| Execution order | First, in registration order | After hooks, no guaranteed order |
| Blocking | Synchronous, blocks next hook | Asynchronous, non-blocking |
| Can cancel | Yes | No (already propagated) |
| Use case | Control flow, validation | Logging, side effects |
// Publisher (service)type OrderService struct { app *application.Application}
func (o *OrderService) CreateOrder(items []Item) (*Order, error) { order := &Order{Items: items}
if err := o.saveOrder(order); err != nil { return nil, err }
// Publish event o.app.Event.Emit("order-created", order)
return order, nil}
// Subscribersapp.Event.On("order-created", func(e *application.CustomEvent) { order := e.Data.(*Order) sendConfirmationEmail(order)})
app.Event.On("order-created", func(e *application.CustomEvent) { order := e.Data.(*Order) updateInventory(order)})
app.Event.On("order-created", func(e *application.CustomEvent) { order := e.Data.(*Order) logOrder(order)})// Frontend requests dataEmit("get-user-data", { userId: 123 })
// Backend respondsapp.Event.On("get-user-data", func(e *application.CustomEvent) { data := e.Data.(map[string]interface{}) userId := int(data["userId"].(float64))
user := getUserFromDB(userId)
// Send response app.Event.Emit("user-data-response", user)})// Frontend receives responseEvents.On("user-data-response", (event) => { const user = event.data displayUser(user)})Note: For request/response, bindings are better. Use events for notifications.
// Broadcast to all windowsapp.Event.Emit("global-notification", "System update available")
// Each window handles itEvents.On("global-notification", (event) => { const message = event.data showNotification(message)})type EventAggregator struct { events []Event mu sync.Mutex}
func (ea *EventAggregator) Add(event Event) { ea.mu.Lock() defer ea.mu.Unlock()
ea.events = append(ea.events, event)
// Emit batch every 100 events if len(ea.events) >= 100 { app.Event.Emit("event-batch", ea.events) ea.events = nil }}Go:
package main
import ( "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/events")
type NotificationService struct { app *application.Application}
func (n *NotificationService) Notify(message string) { // Emit to all windows n.app.Event.Emit("notification", map[string]interface{}{ "message": message, "timestamp": time.Now(), })}
func main() { app := application.New(application.Options{ Name: "Event Demo", })
notifService := &NotificationService{app: app}
// System events app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) { isDark := e.Context().IsDarkMode() app.Event.Emit("theme-changed", isDark) })
// Custom events from frontend app.Event.On("user-action", func(e *application.CustomEvent) { data := e.Data.(map[string]interface{}) action := data["action"].(string)
app.Logger.Info("User action", "action", action)
// Respond notifService.Notify("Action completed: " + action) })
// Window events window := app.Window.New()
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) { app.Event.Emit("window-focused", window.Name()) })
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { // Confirm before close app.Event.Emit("confirm-close", nil) e.Cancel() // Wait for confirmation })
app.Run()}JavaScript:
import { Events } from '@wailsio/runtime'
// Listen for notificationsEvents.On("notification", (event) => { showNotification(event.data.message)})
// Listen for theme changesEvents.On("theme-changed", (event) => { const isDark = event.data document.body.classList.toggle('dark', isDark)})
// Listen for window focusEvents.On("window-focused", (event) => { const windowName = event.data console.log(`Window ${windowName} focused`)})
// Handle close confirmationEvents.On("confirm-close", (event) => { if (confirm("Close window?")) { Emit("close-confirmed", true) }})
// Emit user actionsdocument.getElementById('button').addEventListener('click', () => { Emit("user-action", { action: "button-clicked" })})Custom Events
Create your own event types.
Event Patterns
Common event patterns and best practices.
Bindings
Use bindings for request/response.
Window Events
Handle window lifecycle events.
Questions? Ask in Discord or check the event examples.