Declarative web UI, written in Go.
Compose your interface from widgets — Flutter-inspired. Render the first paint on the server, then hydrate to WebAssembly that drives the browser DOM directly. No npm, no webpack, no node_modules.
package main
import (
"github.com/Runway-Club/gutter"
"github.com/Runway-Club/gutter/themes"
"github.com/Runway-Club/gutter/widgets"
)
type App struct{}
func (App) Build(ctx *gutter.BuildContext) gutter.Widget {
return widgets.Scaffold{
Theme: themes.Apple,
AppBar: widgets.AppBar{Title: "Hello"},
Body: widgets.Center{
Child: widgets.Button{
Variant: widgets.ButtonPrimary,
Label: "Get started",
},
},
}
}
func main() { gutter.RunApp(App{}) }One language, all the way down
Share types and validation between server and UI — the same widget tree renders on the server and hydrates in the browser. The only JavaScript you ship is the tiny wasm_exec.js glue from the Go toolchain.
Pure Go, top to bottom
Widgets, state, and layout are plain Go structs and methods. Your editor, go vet, and the type checker work exactly as they always have.
Compiles to WebAssembly
go build -o app.wasm with GOOS=js GOARCH=wasm. The runtime mounts a persistent element tree and drives the DOM directly.
Theme-driven styling
Pick a variant — ButtonPrimary, CardFeature — and the active theme supplies the values. No CSS in application code.
Tiny bundles with TinyGo
Opt into --tinygo for 4–8× smaller WebAssembly. A counter app drops from ~2.8 MB to ~340 KB.
Surgical re-renders
SetState rebuilds only the subtree that owns the state. Siblings, focused inputs, and scroll positions stay untouched.
Batteries-included CLI
gutter new scaffolds, gutter run dev gives live reload, gutter build deploy emits a Dockerfile + nginx image.
Server-side rendering
Render the first paint as HTML for instant load and SEO, then the same widget tree hydrates to WebAssembly in place. One main() drives both client and server — gutter.Serve.
Typed client↔server RPC
Define request and response structs once in a shared package; rpc.Handle on the server, rpc.Call on the client. No codegen, no stringly-typed routes — renaming a field is a compile error on both sides.
Interactive islands
Drop independent Gutter widgets into an existing HTML or SSR page. Each island lazy-loads the WebAssembly only when it scrolls into view, so mostly-static pages stay light.
This page is the demo
Everything you're looking at is one Gutter widget tree. The counter below is a StatefulWidget — tapping it rebuilds only its own card, leaving the rest of the page (and your scroll position) untouched.
Stateful counter
0
Tapping these only rebuilds this card.
A familiar model
Three widget kinds over a persistent element tree, the way Flutter and React do it. Stateless composes, Stateful owns mutable state, Host maps to one DOM node.
type Counter struct{}
func (Counter) CreateState() gutter.State { return &counterState{} }
type counterState struct {
gutter.StateObject
count int
}
func (s *counterState) Build(ctx *gutter.BuildContext) gutter.Widget {
return widgets.Button{
Label: fmt.Sprintf("Count: %d", s.count),
OnPressed: func() { s.SetState(func() { s.count++ }) },
}
}Embed gutter.StateObject by value; return your state by pointer from CreateState.
SetState(fn) mutates state, then reconciles only this element's subtree.
Anything touching syscall/js lives behind a //go:build js && wasm tag — app code stays platform-neutral.
Ship your first app in a minute
# 1. install the CLI go install github.com/Runway-Club/gutter/cmd/gutter@latest # 2. scaffold a project (add --ssr for a server-rendered + typed-RPC starter) gutter new myapp && cd myapp # 3. run with live reload at http://localhost:8080 gutter run dev
