Wails lets you build cross-platform desktop applications using Go for the backend and any web framework for the frontend — no Electron, no embedded browser, no massive bundle sizes.
Why Wails Matters
Electron apps ship a full Chromium browser (150MB+). Wails uses the OS native webview (WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux), producing binaries under 10MB.
What you get for free:
- Native OS webview — no bundled Chromium
- Go backend with automatic TypeScript bindings
- Hot reload for both Go and frontend code
- Native menus, dialogs, system tray
- Cross-compile for Windows, macOS, Linux from any OS
- App binaries typically 6-10MB vs Electron's 150MB+
Quick Start
# Install Wails CLI
go install github.com/wailsapp/wails/v2/cmd/wails@latest
# Create new project with React
wails init -n myapp -t react-ts
cd myapp
# Development with hot reload
wails dev
# Build production binary
wails build
Go Backend: Expose Functions to Frontend
package main
import (
"context"
"fmt"
"os/exec"
"runtime"
)
type App struct {
ctx context.Context
}
func NewApp() *App {
return &App{}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
// This function is automatically available in TypeScript!
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s! Running on %s", name, runtime.GOOS)
}
// Access system resources that browsers can not
func (a *App) GetSystemInfo() map[string]string {
return map[string]string{
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"cpus": fmt.Sprintf("%d", runtime.NumCPU()),
"version": runtime.Version(),
}
}
// Run shell commands
func (a *App) RunCommand(cmd string) (string, error) {
out, err := exec.Command("sh", "-c", cmd).Output()
return string(out), err
}
TypeScript Frontend: Call Go Functions
// Auto-generated TypeScript bindings!
import { Greet, GetSystemInfo, RunCommand } from "../wailsjs/go/main/App";
function App() {
const [result, setResult] = useState("");
const [sysInfo, setSysInfo] = useState<Record<string, string>>({});
async function greet() {
const greeting = await Greet("Developer");
setResult(greeting);
}
async function loadSystemInfo() {
const info = await GetSystemInfo();
setSysInfo(info);
}
return (
<div>
<button onClick={greet}>Greet</button>
<p>{result}</p>
<button onClick={loadSystemInfo}>System Info</button>
<pre>{JSON.stringify(sysInfo, null, 2)}</pre>
</div>
);
}
Native Features
import "github.com/wailsapp/wails/v2/pkg/runtime"
// File dialogs
func (a *App) OpenFile() (string, error) {
return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Select File",
Filters: []runtime.FileFilter{
{DisplayName: "JSON Files", Pattern: "*.json"},
},
})
}
// System notifications
func (a *App) Notify(title, message string) {
runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Type: runtime.InfoDialog,
Title: title,
Message: message,
})
}
// Window control
func (a *App) Minimize() { runtime.WindowMinimise(a.ctx) }
func (a *App) ToggleFullscreen() { runtime.WindowToggleMaximise(a.ctx) }
Size Comparison
| Framework | Hello World Size | RAM Usage |
|---|---|---|
| Electron | 150-200MB | 80-150MB |
| Tauri | 3-8MB | 20-40MB |
| Wails | 6-10MB | 25-50MB |
| Native Qt | 15-30MB | 30-60MB |
Useful Links
Building data-powered desktop apps? Check out my developer tools on Apify for ready-made web scrapers, or email spinov001@gmail.com for custom solutions.
Top comments (0)