Reactive web UIs in standard HTML and Go

LiveTemplate is a Go library for building reactive web UIs from standard html/template templates. You write a template and a controller struct; when state changes, the template re-renders on the server and only the diff is sent to the browser. The same code runs three ways: a plain <form> POST that reloads the page, a fetch() request that patches the DOM in place, or a WebSocket session where other tabs sync automatically.

Alpha — core features work and are tested, but the API may change before v1.0.

Try it

Counter: 0

Click the buttons. Each click POSTs the action to the Go server; the server runs Increment, re-renders the template, diffs against the previous render, and sends only the changed text node back. The form, the buttons, and the count display are never re-created — only the count's text changes. Open this page in a second tab on the same machine: clicks in one tab show up in the other in real time, because every handler ends with ctx.BroadcastAction(...).

The widget above is a real, deployed LiveTemplate app — the same code as the Your First App tutorial, embedded inline through tinkerdown's auto-proxy.

The code that runs the demo above

The state and handlers — counter.go:

type CounterState struct {
	Counter int
}

// CounterController holds shared dependencies (none in this demo) and
// exposes action methods invoked by name from the template.
type CounterController struct{}

// Increment is invoked when the user clicks the "+1" button. The
// runtime calls it with a clone of the current state and stores
// whatever you return. The BroadcastAction call tells the runtime
// to apply this same action on every other connected client, so
// multiple embeds and tabs stay in lockstep.
func (c *CounterController) Increment(s CounterState, ctx *livetemplate.Context) (CounterState, error) {
	s.Counter++
	ctx.BroadcastAction("Increment", nil)
	return s, nil
}

// Decrement follows the same pattern.
func (c *CounterController) Decrement(s CounterState, ctx *livetemplate.Context) (CounterState, error) {
	s.Counter--
	ctx.BroadcastAction("Decrement", nil)
	return s, nil
}

counter.go:9-33

The template — counter.tmpl:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Counter</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@livetemplate/client@latest/livetemplate.css">
    <script defer src="https://cdn.jsdelivr.net/npm/@livetemplate/client@latest/dist/livetemplate-client.browser.js"></script>
</head>
<body>
    <main class="container">
        <h1>Counter: {{.Counter}}</h1>
        <form method="POST" style="display:inline">
            <button name="increment">+1</button>
            <button name="decrement" class="secondary">-1</button>
        </form>
    </main>
</body>
</html>

counter.tmpl

A button's name attribute IS the routing key — <button name="increment"> posts increment and LiveTemplate dispatches to the Increment method on the controller. The protocol between HTML and Go is just the form data the browser already sends.

Read the full walkthrough → — or jump to Counter, deeper for the production-shaped story (broadcast routing, session models, scaling).

What happens between a click and a DOM update

sequenceDiagram
    participant Browser
    participant Server

    Browser->>Server: User clicks button<br/>{action: "add", form: {title: "Buy milk"}}
    Note over Server: Add() returns new state<br/>(Items: [...] → [..., new])
    Note over Server: Tree diff calculated<br/>Only changed values sent
    Server->>Browser: {patches: [...]}
    Note over Browser: DOM patched in place<br/>(no full re-render)

When a user clicks a button, LiveTemplate calls a method on your Go struct, diffs the template output against the previous render, and sends only what changed.

See the full architecture walkthrough →

Get started

  1. Installgo get, ~30 seconds
  2. Your First App — counter app from scratch in 10 minutes
  3. Progressive Complexity — when to reach for lvt-* attributes (and when not to)
  4. Recipes — basics, UI patterns, runnable apps, and deep dives

Or browse

How this site is built

This is a tinkerdown site. Most reference and package pages are mirrored from canonical files in the source repos (livetemplate, client, lvt, examples) and re-published on each release. Recipe apps and UI pattern recipes are served by the docs-site recipes binary so the examples stay interactive inside the docs. The "Edit this page on GitHub" link in every footer points to the canonical source — that's where corrections should land. See How This Docs Site Works for the full dogfood loop.