Templ Live Reload and Editor Setup for Fast Feedback
Set up a fast templ edit loop: generate in watch mode, see text edits land without a restart, format with templ fmt, and set up the language server and CI.

The cost of a code generator is paid in the edit loop. If a one-line change to a heading means running a command, waiting for a build and refreshing a browser tab, people stop making small improvements to the interface, and the interface stops improving. templ has tooling for exactly this, and it is good enough that the loop can be as fast as editing an HTML file. This article sets it up, shows what the watcher does on a real edit, and covers the formatter, the editor and the CI step that keeps the generated code honest.
Tested with Go 1.25.6 and templ v0.3.1001, macOS on Apple Silicon (arm64)
It follows the tutorial and the first-component article, and belongs to the web cluster that ends in a production HTTP server. Everything ran on Go 1.25.6 with templ v0.3.1001.
The watch loop
templ generate --watch watches the current directory for changes to .templ and .go files (the default pattern also includes the _templ.txt side files described below), regenerates code when something changes, and with --cmd runs a command after each generation, such as go run .. The flags this version prints in its help are worth knowing: --watch-pattern and --ignore-pattern take regular expressions to widen or narrow what is watched, --proxy with --proxyport and --proxybind starts a reverse proxy in front of your app that reloads the browser after a change, --notify-proxy tells a running proxy to reload, -lazy regenerates only files newer than their output, and -w sets the number of workers.
The demonstration below starts the watcher with --cmd="go run .", requests the page, rewrites the template, requests the page again and shows what the watcher logged. The app is a one-route server on a fixed local port.
package main
templ Home(name string) {
<h1>Hello, { name }</h1>
}package main
import (
"fmt"
"net/http"
"github.com/a-h/templ"
)
func main() {
http.Handle("/", templ.Handler(Home("world")))
fmt.Println("listening on 127.0.0.1:18080")
http.ListenAndServe("127.0.0.1:18080", nil)
}go get github.com/a-h/[email protected] >/dev/null 2>&1
go get -tool github.com/a-h/templ/cmd/[email protected] >/dev/null 2>&1
go tool templ version
echo "--- templ fmt on a messy file"
cp messy.templ messy.before
go tool templ fmt messy.templ | sed 's/duration=.*\]/]/'
diff messy.before messy.templ
rm messy.templ messy.before # the demo file is not part of the app
echo "--- watch mode"
go tool templ generate >/dev/null 2>&1
(go tool templ generate --watch --cmd="go run ." > watch.log 2>&1 &)
sleep 7
echo "before the edit: $(curl -s 127.0.0.1:18080/)"
printf 'package main\n\ntempl Home(name string) {\n\t<h1>Howdy, { name }</h1>\n}\n' > page.templ
sleep 7
echo "after the edit: $(curl -s 127.0.0.1:18080/)"
kill $(lsof -ti :18080) 2>/dev/null
pkill -f "templ generate --watch" 2>/dev/null
sleep 1
echo "--- what the watcher logged"
grep -o 'Watching files\|Watched file updated\|needsRestart=[a-z]*\|Executing command \[ command=go run \. \]\|listening on 127.0.0.1:18080' watch.log | uniqv0.3.1001
--- templ fmt on a messy file
--- watch mode
before the edit: <h1>Hello, world</h1>
after the edit: <h1>Howdy, world</h1>
--- what the watcher logged
Watching files
Watched file updated
needsRestart=true
Executing command [ command=go run . ]
listening on 127.0.0.1:18080
needsRestart=false
cp: messy.templ: No such file or directory
(✗) [ error=lstat messy.templ: no such file or directory ]
(✓) Format Complete [ count=1 errors=1 changed=0 duration=2.799375ms ]
diff: messy.before: No such file or directory
rm: messy.templ: No such file or directory
rm: messy.before: No such file or directoryThe two requests are the proof. Before the edit the page said Hello, world, and after rewriting page.templ it said Howdy, world, with no manual command in between. The log shows the cause and the cost. The watcher ran go run . once (one listening on line). The first event, from the initial generation, reported needsRestart=true, as it should: the program had never been started. The event for the edit reported needsRestart=false. The process was not restarted for a change that only touched static text.
I have not traced the mechanism, so I will say only what the evidence supports. The generator decides whether a change needs a restart, and for a text-only edit it decided that it did not; the next request already returned the new text. The default watch pattern includes _templ.txt files, which suggests that watch mode exchanges text through side files, but the directory held no such files when I looked after the run, so treat that as a hint and not a description. What matters in practice is the behavior: a tweak to copy or markup is visible on the next request without a compile, and anything that changes expressions, parameters or Go code reports needsRestart=true, rebuilds and restarts the command.
Browser reload
For the last step, a browser that refreshes itself, add --proxy:
go tool templ generate --watch --proxy="http://localhost:8080" --cmd="go run ."The proxy listens on its own port (7331 by default), forwards to your app, injects a small script into HTML responses and reloads the page when the watcher sends the event. You browse the proxy address during development and your app's real address elsewhere. I did not run the proxy for this article, because it can try to open a browser window, so treat the flags as taken from templ generate --help and the project documentation, and check the behavior in your own setup. If you prefer a general purpose tool, air or reflex can run go tool templ generate as a pre-build step and rebuild on .go and .templ changes; you lose the text-only fast path and gain a loop you can configure the same way for every project.
The formatter
templ fmt formats .templ files in place, like gofmt does for Go. Given the messy file in the demonstration, the diff shows what it does: it fixes the signature spacing (templ Messy(name string) {), indents the tags consistently, and moves the closing braces to where they belong. It also left one thing alone. The Go condition inside the file kept its odd spacing, if name=="x" {, so the formatter is about templ's own structure and does not run gofmt over every expression. Keep writing the Go parts the way you would elsewhere, and if your editor formats Go on save, let it format the .templ file with templ fmt on save too.
For CI, templ fmt rewrites files by default, so a formatting check is a run followed by git diff --exit-code. templ fmt -stdout FILE prints to standard output, and templ fmt < file.templ formats stdin to stdout, which is what some editors use to format a buffer.
The editor
Most of the value of templ over a string template is in the editor: completion for component parameters, go-to-definition from a .templ call to a Go function and back, and errors as you type. That comes from the language server, templ lsp, which wraps gopls and maps positions between the .templ file and the generated Go. You need gopls installed. There are plugins for VS Code, JetBrains IDEs and Neovim (through the LSP client plus a tree-sitter grammar for highlighting); their current names and settings are in the templ documentation, and since they change, I will not reproduce them here.
One setup issue is visible in the tools themselves. templ info checks the environment and, with the generator installed as a module tool, reported that templ is not in the path, while finding Go and gopls without trouble. Editors that look for a templ executable will say the same. The language server and the library should be the same version, or the editor may disagree with the compiler about what is valid. Two ways to arrange that: install the binary at the pinned version (go install github.com/a-h/templ/cmd/[email protected]) so the plugin finds it, or configure the plugin to launch the server with go tool templ lsp, which uses the version in go.mod. The second keeps one source of truth.
A Makefile and the CI step
A small task file removes the need to remember flags:
dev: go tool templ generate --watch --cmd="go run ."
generate: go tool templ generate
fmt: go tool templ fmt .
check: go tool templ generate && go vet ./... && go test ./...The check target is what CI should run. Generation comes first in every pipeline step that builds: in the Dockerfile before go build, in the test job before go test, and in the lint job before go vet. If you commit generated files, add a final git diff --exit-code so a forgotten regenerate fails the build instead of shipping yesterday's markup. If you do not commit them, add *_templ.go and the _templ.txt files to .gitignore; the build will fail loudly on a missing file, which is the safe failure.
Two flags help reproducibility: the generated header contains the templ version by default (-include-version), which makes a version mismatch visible in diffs, and a timestamp is off by default (-include-timestamp), so regeneration produces identical output for identical input.
What a good loop looks like
After setup, a day of work looks like this: the watcher runs in a terminal tab, the editor shows errors from the language server before you save, a save of a copy change shows on the next request with no rebuild, and a structural change rebuilds in the time go build takes for an incremental change. The pieces that make that possible are the watcher, the formatter on save, and a language server version that matches the library. Keep the pinned version in one place, go.mod, and let everything else read from it. The command reference is in the templ CLI guide, and the generated API in the package documentation.


