Overview
Wails is a framework for building desktop applications using Go and web technologies. Unlike Electron, which bundles a full Chromium browser, Wails uses the native webview component of each operating system (WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux), resulting in significantly smaller binary sizes (typically 5-10MB vs 150MB+ for Electron) and lower memory usage. Your Go code runs natively, and the frontend communicates with it through auto-generated TypeScript bindings.
Created by Lea Anthony, Wails v2 provides a production-ready framework for building cross-platform desktop applications. It supports any frontend framework (React, Vue, Svelte, etc.) or plain HTML/CSS/JS. Go methods can be bound to the frontend and called directly from JavaScript with full type safety. Wails also provides native OS integration including menus, dialogs, system tray, notifications, and window management.
Installation
Prerequisites
# Go 1.21+ required
go version
# Install Wails CLI
go install github.com/wailsapp/wails/v2/cmd/wails@latest
# Check system dependencies
wails doctor
# macOS - Xcode command line tools
xcode-select --install
# Ubuntu/Debian
sudo apt install libgtk-3-dev libwebkit2gtk-4.0-dev
# Fedora
sudo dnf install gtk3-devel webkit2gtk4.0-devel
# Arch
sudo pacman -S webkit2gtk gtk3
# Windows - WebView2 Runtime (usually pre-installed on Windows 10/11)
# If missing: https://developer.microsoft.com/en-us/microsoft-edge/webview2/
Creating a Project
Project Templates
# Default (vanilla JS)
wails init -n myapp
# With React
wails init -n myapp -t react-ts
# With Vue
wails init -n myapp -t vue-ts
# With Svelte
wails init -n myapp -t svelte-ts
# With Preact
wails init -n myapp -t preact-ts
# List all templates
wails init -l
Project Structure
| Path | Description |
|---|
main.go | Application entry point |
app.go | Application struct with bound methods |
wails.json | Wails project configuration |
frontend/ | Frontend web application |
frontend/src/ | Frontend source code |
frontend/wailsjs/ | Auto-generated Go bindings |
build/ | Build artifacts |
build/appicon.png | Application icon |
Development
Dev Commands
| Command | Description |
|---|
wails dev | Start dev mode with hot reload |
wails build | Build production binary |
wails build -o myapp | Build with custom output name |
wails build -platform windows | Cross-compile for Windows |
wails build -platform darwin | Build for macOS |
wails build -platform linux | Build for Linux |
wails build -nsis | Build Windows installer (NSIS) |
wails build -upx | Compress binary with UPX |
wails doctor | Check system dependencies |
wails generate module | Regenerate bindings |
# Dev mode with browser devtools
wails dev
# Dev mode opening in browser (for browser devtools)
wails dev -browser
# Build optimized production binary
wails build -clean -trimpath -ldflags "-s -w"
Backend (Go)
Application Structure
// main.go
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/linux"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2/pkg/options/windows"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
app := NewApp()
err := wails.Run(&options.App{
Title: "My App",
Width: 1024,
Height: 768,
MinWidth: 800,
MinHeight: 600,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
OnShutdown: app.shutdown,
OnDomReady: app.domReady,
Bind: []interface{}{
app,
},
Mac: &mac.Options{
TitleBar: &mac.TitleBar{
TitlebarAppearsTransparent: true,
HideTitle: false,
HideTitleBar: false,
FullSizeContent: true,
},
},
Windows: &windows.Options{
WebviewIsTransparent: false,
WindowIsTranslucent: false,
},
Linux: &linux.Options{
ProgramName: "My App",
},
})
if err != nil {
println("Error:", err.Error())
}
}
Binding Go Methods
// app.go
package main
import (
"context"
"fmt"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type App struct {
ctx context.Context
}
func NewApp() *App {
return &App{}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
func (a *App) shutdown(ctx context.Context) {
// Cleanup
}
func (a *App) domReady(ctx context.Context) {
// DOM is ready
}
// Exported methods are callable from frontend
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, it's %s!", name, time.Now().Format("3:04 PM"))
}
// Struct types work too
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func (a *App) GetUser(id int) (*User, error) {
// Fetch from database etc.
return &User{
Name: "Alice",
Email: "alice@example.com",
Age: 30,
}, nil
}
func (a *App) SaveUser(user User) error {
// Save to database
fmt.Printf("Saving user: %+v\n", user)
return nil
}
Runtime APIs
// Dialogs
result, err := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "Confirm",
Message: "Are you sure?",
Buttons: []string{"Yes", "No"},
})
// File dialogs
filepath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Select File",
Filters: []runtime.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.jpeg"},
{DisplayName: "All Files", Pattern: "*.*"},
},
})
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "Save As",
DefaultFilename: "export.json",
})
// Window control
runtime.WindowSetTitle(a.ctx, "New Title")
runtime.WindowSetSize(a.ctx, 1280, 720)
runtime.WindowCenter(a.ctx)
runtime.WindowMinimise(a.ctx)
runtime.WindowMaximise(a.ctx)
runtime.WindowToggleMaximise(a.ctx)
runtime.WindowFullscreen(a.ctx)
runtime.WindowUnfullscreen(a.ctx)
runtime.WindowHide(a.ctx)
runtime.WindowShow(a.ctx)
// Events (Go -> Frontend)
runtime.EventsEmit(a.ctx, "data-updated", data)
// Events (Frontend -> Go listener)
runtime.EventsOn(a.ctx, "frontend-event", func(data ...interface{}) {
fmt.Println("Received:", data)
})
// Clipboard
runtime.ClipboardSetText(a.ctx, "copied text")
text, _ := runtime.ClipboardGetText(a.ctx)
// Environment
env := runtime.Environment(a.ctx)
fmt.Println(env.Platform) // "darwin", "windows", "linux"
Frontend Integration
Calling Go from JavaScript/TypeScript
// Auto-generated bindings in frontend/wailsjs/go/main/App.ts
import { Greet, GetUser, SaveUser } from '../wailsjs/go/main/App';
import { main } from '../wailsjs/go/models';
// Call Go methods
async function greet() {
const result = await Greet("World");
console.log(result); // "Hello World, it's 3:04 PM!"
}
// With typed models
async function loadUser() {
const user: main.User = await GetUser(1);
console.log(user.name, user.email);
}
// Send data to Go
async function saveUser() {
const user = new main.User();
user.name = "Bob";
user.email = "bob@example.com";
user.age = 25;
await SaveUser(user);
}
Wails Runtime (Frontend)
import { EventsOn, EventsEmit, EventsOff } from '../wailsjs/runtime/runtime';
// Listen for Go events
EventsOn("data-updated", (data: any) => {
console.log("Data updated:", data);
});
// Emit events to Go
EventsEmit("frontend-event", { action: "click", target: "button" });
// Cleanup listener
EventsOff("data-updated");
// Window drag (for frameless windows)
// Add wails-drag attribute to HTML element:
// <div data-wails-drag>Drag me</div>
React Example
import { useState, useEffect } from 'react';
import { Greet, GetUser } from '../wailsjs/go/main/App';
import { EventsOn } from '../wailsjs/runtime/runtime';
function App() {
const [greeting, setGreeting] = useState('');
const [name, setName] = useState('');
useEffect(() => {
const cleanup = EventsOn("notification", (msg: string) => {
alert(msg);
});
return () => cleanup();
}, []);
const handleGreet = async () => {
const result = await Greet(name);
setGreeting(result);
};
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={handleGreet}>Greet</button>
<p>{greeting}</p>
</div>
);
}
Configuration
wails.json
{
"$schema": "https://wails.io/schemas/config.v2.json",
"name": "myapp",
"outputfilename": "myapp",
"frontend:install": "npm install",
"frontend:build": "npm run build",
"frontend:dev:watcher": "npm run dev",
"frontend:dev:serverUrl": "auto",
"author": {
"name": "Developer",
"email": "dev@example.com"
},
"info": {
"companyName": "My Company",
"productName": "My App",
"productVersion": "1.0.0",
"copyright": "Copyright 2024",
"comments": "Built with Wails"
}
}
Advanced Usage
import "github.com/wailsapp/wails/v2/pkg/menu"
import "github.com/wailsapp/wails/v2/pkg/menu/keys"
func (a *App) createMenu() *menu.Menu {
appMenu := menu.NewMenu()
fileMenu := appMenu.AddSubmenu("File")
fileMenu.AddText("Open", keys.CmdOrCtrl("o"), func(cd *menu.CallbackData) {
// Handle open
})
fileMenu.AddSeparator()
fileMenu.AddText("Quit", keys.CmdOrCtrl("q"), func(cd *menu.CallbackData) {
runtime.Quit(a.ctx)
})
return appMenu
}
System Tray
// Wails v3 (upcoming) has native system tray support
// For v2, use third-party packages like systray
Troubleshooting
| Problem | Solution |
|---|
wails doctor fails | Install missing platform dependencies listed in output |
| Bindings not generated | Run wails generate module; check Go methods are exported |
| White screen in dev | Check frontend dev server is running; verify URL in console |
| Build fails on Linux | Install libgtk-3-dev and libwebkit2gtk-4.0-dev |
| Window not showing | Check Width/Height in options; verify OnStartup doesn’t panic |
| Cross-compile fails | Use Docker or CI for cross-platform builds |
| Large binary size | Use -trimpath -ldflags "-s -w" and -upx flags |
| Hot reload not working | Ensure frontend:dev:watcher is correct in wails.json |