Reactive web UIs in standard HTML and Go. No custom template language. No client-side framework. No persistent connection required.
📚 Documentation: https://livetemplate.fly.dev — guides, recipes, patterns catalog, full reference.
Quick Start | Docs site | Examples | API Reference
Alpha — Core features work and are tested, but the API may change before v1.0.
The HTML and Go behind a reactive todo list:
<form method="POST">
<input type="text" name="title" required placeholder="What needs to be done?">
<button name="add">Add Todo</button>
</form>
<ul>
{{range .Items}}<li>{{.Title}}</li>{{end}}
</ul>func (c *TodoController) Mount(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
_ = ctx.Subscribe(ctx.SelfTopic()) // opt in to peer fan-out on this session
return state, nil
}
func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
state.Items = append(state.Items, Todo{Title: ctx.GetString("title")})
ctx.Publish(ctx.SelfTopic(), "Refresh", nil) // pushes update to other WS-connected tabs
return state, nil
}The button's name IS the action — <button name="add"> routes to Add(). No custom attributes, no JavaScript wiring. Without JS, the form POSTs normally. With the JS client, the DOM patches in place. Add a WebSocket and other tabs sync automatically. See Standard HTML Reactivity for how this compares to htmx, Livewire, and LiveView.
The full todos example uses SQLite, form validation, and toasts. The snippet above is the minimal in-memory shape.
sequenceDiagram
participant Browser
participant Server
Browser->>Server: User clicks button<br/>{action: "increment"}
Note over Server: s.Counter++<br/>(Counter: 5 → 6)
Note over Server: Tree diff calculated<br/>Only Counter changed → {"0": "6"}
Server->>Browser: {"0": "6"}
Note over Browser: DOM updated<br/>Counter: 6
When a user clicks a button, LiveTemplate calls a method on your Go struct, diffs the template output, and sends only what changed.
go get github.com/livetemplate/livetemplate1. Define controller and state (full example)
type CounterState struct {
Counter int
}
type CounterController struct{}
func (c *CounterController) Increment(state CounterState, ctx *livetemplate.Context) (CounterState, error) {
state.Counter++
return state, nil
}
func (c *CounterController) Decrement(state CounterState, ctx *livetemplate.Context) (CounterState, error) {
state.Counter--
return state, nil
}
func main() {
controller := &CounterController{}
state := &CounterState{Counter: 0}
tmpl := livetemplate.Must(livetemplate.New("counter"))
http.Handle("/", tmpl.Handle(controller, livetemplate.AsState(state)))
http.ListenAndServe(":8080", nil)
}New auto-discovers *.tmpl files in the current directory — counter.tmpl is picked up automatically.
2. Write the template (counter.tmpl)
<h1>Counter: {{.Counter}}</h1>
<form method="POST" style="display:inline">
<button name="increment">+</button>
<button name="decrement">-</button>
</form>
<link rel="stylesheet" href="{{lvtClientStyleURL}}">
<script defer src="{{lvtClientScriptURL}}"></script>lvtClientStyleURL / lvtClientScriptURL are framework-provided template functions that render the CDN URL for the @livetemplate/client release this LiveTemplate version is wire-compatible with — so the browser client stays pinned in lockstep with the server instead of drifting on @latest. To self-host, replace them with your own tag pointing at a vendored @livetemplate/client@<version>.
3. Run it
go run main.go # Open http://localhost:8080Standard HTML first — Forms, buttons, dialogs, and links work reactively without custom attributes. lvt-* attributes are available for behaviors HTML can't express (debounce, keyboard shortcuts, reactive DOM). Guide →
Safe state management — Controllers (singleton, hold dependencies) are separated from state (pure data, cloned per session). No accidental data leakage between users. Reference →
Efficient updates — Templates split into static structure (cached) and dynamic values. Updates send only changed values — typically 85%+ bandwidth savings. Details →
Idiomatic Go errors — Actions return (State, error). Validation errors flow to templates automatically. No error serialization code. Error handling →
Code generation — lvt new myapp && lvt gen resource products name price:float scaffolds full CRUD apps with reactive UIs. CLI →
Guides:
- Standard HTML Reactivity — How LiveTemplate compares to htmx, Livewire, LiveView
- Progressive Complexity — Standard HTML →
lvt-*attributes - Scaling — Redis-backed sessions, horizontal scaling
References:
- Controller+State Pattern — Core architecture
- Client Attributes —
lvt-*reference - Navigate Action —
__navigate__reserved action invariants - Error Handling — Validation and errors
- Configuration — Options and environment variables
- Current Limitations — Known gaps and workarounds
Related Projects:
- CLI Tool (lvt) — Code generator and dev server
- Client Library — TypeScript client (npm:
@livetemplate/client) - Examples — Counter, Todos, Chat, and more
- Tinkerdown — Build data-driven apps from a single markdown file (built on LiveTemplate)
New to the codebase? Start with the Contributor Walkthrough.
See CONTRIBUTING.md for development setup and guidelines.
MIT License — see LICENSE file for details.