Quick Start
Get started with Wails v3.
Wails v3 is a complete rewrite with significant improvements in architecture, performance, and developer experience. This guide helps you migrate your v2 application to v3.
Key changes:
Migration time: 1-4 hours for typical applications
In v2, application setup, window configuration, and execution were all combined into a single wails.Run() call. This monolithic approach made it difficult to create multiple windows, handle errors at different stages, or test individual components of your application.
v3 separates these concerns into distinct phases: application creation, window creation, and execution. This separation gives you explicit control over each stage of your application’s lifecycle and makes the code more modular and testable.
v2:
err := wails.Run(&options.App{ Title: "My App", Width: 1024, Height: 768, Bind: []interface{}{ &GreetService{}, },})v3:
app := application.New(application.Options{ Name: "My App", Services: []application.Service{ application.NewService(&GreetService{}), },})
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "My App", Width: 1024, Height: 768,})
app.Run()Why this is better:
In v2, every bound struct required a context field and a startup(ctx) method to receive the runtime context. This created tight coupling between your business logic and the Wails runtime, making code harder to test and understand.
v3 introduces the service pattern, where your structs are completely standalone and don’t need to store runtime context. If a service needs access to the application instance, it explicitly receives it through dependency injection rather than implicit context threading.
v2:
type App struct { ctx context.Context}
func (a *App) startup(ctx context.Context) { a.ctx = ctx}
func (a *App) Greet(name string) string { return "Hello " + name}v3:
type GreetService struct{}
func (g *GreetService) Greet(name string) string { return "Hello " + name}
// Register as serviceapp := application.New(application.Options{ Services: []application.Service{ application.NewService(&GreetService{}), },})Why this is better:
App structServiceStartup() method when you need initialization, making it explicitIn v2, all runtime operations required passing a context to global functions from the runtime package. This created tight coupling to the context object throughout your codebase and made the API feel procedural rather than object-oriented.
v3 replaces the context-based runtime with direct method calls on application and window objects. Operations are called directly on the objects they affect, making the code more intuitive and object-oriented.
v2:
import "github.com/wailsapp/wails/v2/pkg/runtime"
runtime.WindowSetTitle(a.ctx, "New Title")runtime.EventsEmit(a.ctx, "event-name", data)v3:
// Store app referencetype MyService struct { app *application.Application}
func (s *MyService) UpdateTitle() { window := s.app.Window.Current() window.SetTitle("New Title")}
func (s *MyService) EmitEvent() { s.app.Event.Emit("event-name", data)}Why this is better:
window.SetTitle() is more obvious than runtime.WindowSetTitle(ctx, ...)In v2, bindings were organized by Go package and struct name, typically resulting in paths like wailsjs/go/main/App. This structure didn’t reflect logical grouping and made it hard to find related functionality.
v3 organizes bindings by service name and application module, creating a clearer logical structure. The bindings are generated into a bindings directory organized by your application name and service names, making it easier to understand what functionality is available.
v2:
import { Greet } from '../wailsjs/go/main/App'
const result = await Greet("World")v3:
import { Greet } from './bindings/myapp/greetservice'
const result = await Greet("World")Why this is better:
../wailsjs/go prefix - just ./bindingsIn v2, events used variadic interface{} parameters and required passing context to every event function. Event handlers received untyped data that needed manual type assertions, making the event system error-prone and hard to debug.
v3 introduces typed event objects and removes the context requirement. Event handlers receive a proper event object with typed data, making the event system more reliable and easier to use.
v2:
runtime.EventsOn(ctx, "event-name", func(data ...interface{}) { // Handle event})
runtime.EventsEmit(ctx, "event-name", data)v3:
app.Event.On("event-name", func(e *application.CustomEvent) { data := e.Data // Handle event})
app.Event.Emit("event-name", data)Why this is better:
...interface{}app.Event.On() and app.Event.Emit() are more intuitive than runtime functionsv2 supported only a single window per application. The window was created at startup and all window operations were performed through runtime functions that implicitly targeted that single window.
v3 introduces native multi-window support as a core feature. Each window is a first-class object with its own methods and lifecycle. You can create, manage, and destroy multiple windows dynamically throughout your application’s lifetime.
v2:
// Single window onlyruntime.WindowSetSize(ctx, 800, 600)v3:
// Multiple windows supportedwindow1 := app.Window.New()window1.SetSize(800, 600)
window2 := app.Window.New()window2.SetSize(1024, 768)Why this is better:
go.mod:
module myapp
go 1.21
require ( github.com/wailsapp/wails/v3 v3.0.0-alpha.1)Update:
go get github.com/wailsapp/wails/v3@latestgo mod tidyv2:
package main
import ( "embed" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/windows")
//go:embed all:frontend/distvar assets embed.FS
func main() { app := NewApp()
err := wails.Run(&options.App{ Title: "My App", Width: 1024, Height: 768, AssetServer: &assetserver.Options{ Assets: assets, }, Bind: []interface{}{ app, }, Windows: &windows.Options{ WebviewIsTransparent: false, }, })
if err != nil { println("Error:", err.Error()) }}v3:
package main
import ( "embed" "github.com/wailsapp/wails/v3/pkg/application")
//go:embed frontend/distvar assets embed.FS
func main() { app := application.New(application.Options{ Name: "My App", Services: []application.Service{ application.NewService(&MyService{}), }, Assets: application.AssetOptions{ Handler: application.AssetFileServerFS(assets), }, })
app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "My App", Width: 1024, Height: 768, })
err := app.Run() if err != nil { panic(err) }}v2:
type App struct { ctx context.Context}
func NewApp() *App { return &App{}}
func (a *App) startup(ctx context.Context) { a.ctx = ctx // Initialisation}
func (a *App) Greet(name string) string { return "Hello " + name}v3:
type MyService struct { app *application.Application}
func NewMyService(app *application.Application) *MyService { return &MyService{app: app}}
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { // Initialisation return nil}
func (s *MyService) Greet(name string) string { return "Hello " + name}
// Register after app creationapp := application.New(application.Options{})app.RegisterService(application.NewService(NewMyService(app)))v2:
func (a *App) DoSomething() { runtime.WindowSetTitle(a.ctx, "New Title") runtime.EventsEmit(a.ctx, "update", data) runtime.LogInfo(a.ctx, "Message")}v3:
func (s *MyService) DoSomething() { window := s.app.Window.Current() window.SetTitle("New Title")
s.app.Event.Emit("update", data)
s.app.Logger.Info("Message")}Generate new bindings:
wails3 generate bindingsUpdate imports:
// v2import { Greet } from '../wailsjs/go/main/App'
// v3import { Greet } from './bindings/myapp/myservice'Update event handling:
// v2import { EventsOn, EventsEmit } from '../wailsjs/runtime/runtime'
EventsOn("update", (data) => { console.log(data)})
EventsEmit("action", data)
// v3import { Events } from '@wailsio/runtime'
Events.On("update", (data) => { console.log(data)})
Events.Emit("action", data)v2 (wails.json):
{ "name": "myapp", "outputfilename": "myapp", "frontend:install": "npm install", "frontend:build": "npm run build", "frontend:dev:watcher": "npm run dev", "frontend:dev:serverUrl": "auto"}v3 (wails.json):
{ "name": "myapp", "frontend": { "dir": "./frontend", "install": "npm install", "build": "npm run build", "dev": "npm run dev", "devServerUrl": "http://localhost:5173" }}v2:
selection, err := runtime.OpenFileDialog(ctx, runtime.OpenDialogOptions{ Title: "Select File",})v3:
selection, err := app.Dialog.OpenFile(application.OpenFileDialogOptions{ Title: "Select File",})v2:
menu := menu.NewMenu()menu.Append(menu.Text("File", nil, []*menu.MenuItem{ menu.Text("Quit", nil, func(_ *menu.CallbackData) { runtime.Quit(ctx) }),}))v3:
menu := app.NewMenu()fileMenu := menu.AddSubmenu("File")fileMenu.Add("Quit").OnClick(func(ctx *application.Context) { app.Quit()})v2:
// Not available in v2v3:
systray := app.SystemTray.New()systray.SetIcon(iconBytes)systray.SetLabel("My App")
menu := app.NewMenu()menu.Add("Show").OnClick(showWindow)menu.Add("Quit").OnClick(app.Quit)systray.SetMenu(menu)Problem: Import errors after migration
Solution:
# Regenerate bindingswails3 generate bindings
# Check output directoryls frontend/bindingsProblem: ctx not available
Solution:
Store app reference instead:
type MyService struct { app *application.Application}
func NewMyService(app *application.Application) *MyService { return &MyService{app: app}}Problem: runtime.WindowSetTitle() doesn’t exist
Solution:
Use window methods directly:
window := s.app.Window.Current()window.SetTitle("New Title")Problem: Events registered but not received
Solution:
Check event names match exactly:
// Goapp.Event.Emit("my-event", data)
// JavaScriptOnEvent("my-event", handler) // Must match exactly# Developmentwails3 dev
# Buildwails3 build
# Generate bindingswails3 generate bindingsQ: Can I run v2 and v3 side by side?
A: Yes, they use different import paths.
Q: Is v3 production-ready?
A: v3 is in alpha/beta. Test thoroughly before production.
Q: Will v2 be maintained?
A: Yes, v2 will receive critical updates.
Q: How long does migration take?
A: 1-4 hours for typical applications.
Quick Start
Get started with Wails v3.
Core Concepts
Understand v3 architecture.
Bindings
Learn the new bindings system.
Examples
See complete v3 examples.
Questions? Ask in Discord or open an issue.