Wails is a framework for building desktop applications using Go for the backend and web technologies for the frontend. Like Tauri but for Go developers — it uses the OS webview for tiny binaries.
Why Wails?
- Go backend — full power of Go standard library + ecosystem
- Any frontend — React, Vue, Svelte, Lit, vanilla JS
- Small binaries — ~8MB (vs Electron's 150MB)
- Native menus — OS-native menus, dialogs, file pickers
- Cross-platform — Windows, macOS, Linux
Install & Create
go install github.com/wailsapp/wails/v2/cmd/wails@latest
# Create project (React template)
wails init -n my-app -t react-ts
cd my-app
# Dev mode with live reload
wails dev
# Build
wails build
Go Backend (Exposed to Frontend)
// app.go
package main
import (
"context"
"fmt"
"os"
)
type App struct {
ctx context.Context
}
func NewApp() *App {
return &App{}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
// Exposed to frontend automatically!
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, it's nice to meet you!", name)
}
func (a *App) ReadFile(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(data), nil
}
func (a *App) GetSystemInfo() map[string]string {
hostname, _ := os.Hostname()
return map[string]string{
"hostname": hostname,
"os": runtime.GOOS,
"arch": runtime.GOARCH,
}
}
Frontend (TypeScript)
// frontend/src/App.tsx
import { Greet, ReadFile, GetSystemInfo } from '../wailsjs/go/main/App';
function App() {
const [greeting, setGreeting] = useState('');
async function handleGreet() {
const result = await Greet('World');
setGreeting(result);
}
async function handleSystemInfo() {
const info = await GetSystemInfo();
console.log(info); // { hostname: '...', os: 'darwin', arch: 'arm64' }
}
return (
<div>
<button onClick={handleGreet}>Greet</button>
<p>{greeting}</p>
<button onClick={handleSystemInfo}>System Info</button>
</div>
);
}
Events (Bi-directional)
// Go: emit event to frontend
import "github.com/wailsapp/wails/v2/pkg/runtime"
func (a *App) StartLongTask() {
for i := 0; i <= 100; i++ {
runtime.EventsEmit(a.ctx, "progress", i)
time.Sleep(50 * time.Millisecond)
}
runtime.EventsEmit(a.ctx, "complete", "Done!")
}
// Frontend: listen for events
import { EventsOn } from '../wailsjs/runtime';
EventsOn('progress', (value: number) => {
console.log(`Progress: ${value}%`);
});
EventsOn('complete', (msg: string) => {
console.log(msg);
});
Native Dialogs
import "github.com/wailsapp/wails/v2/pkg/runtime"
func (a *App) OpenFile() (string, error) {
file, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Select a file",
Filters: []runtime.FileFilter{
{DisplayName: "Text Files", Pattern: "*.txt;*.md"},
},
})
return file, err
}
func (a *App) ShowMessage(title, message string) {
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Type: runtime.InfoDialog,
Title: title,
Message: message,
})
}
Comparison
| Wails | Tauri | Electron | |
|---|---|---|---|
| Backend | Go | Rust | Node.js |
| Binary size | ~8 MB | ~600 KB | ~150 MB |
| RAM | ~40 MB | ~30 MB | ~150 MB |
| Learning curve | Low (Go) | Medium (Rust) | Low (JS) |
Resources
Building desktop or web tools? Check my Apify actors or email spinov001@gmail.com.
Top comments (0)