Building Modern Web Apps with Go templ + htmx (2026)
Introduction
A decade of SPA dominance, but 2026 teams are reassessing: 90% of dashboards and content sites don't need client routing. SEO and first paint matter. Full-stack teams should use one language. Go templ + htmx: compile-time safe templates + HTML-driven interactions — no JS.
templ: type-safe Go templates
html/template parses at runtime — errors surface on render. templ compiles to Go — compile-time detection.
package views
templ Header(title string) { <header><h1>{ title }</h1></header> }
templ Layout(title string) {
<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@2.0.4"></script></head>
<body>@Header(title)<main>{ children... }</main></body></html>
}
{ title } type-checked. { children... } slots. Zero runtime overhead — compile-time inlining.
htmx: hypermedia-driven
"Any HTML element issues HTTP and swaps responses" — no JS.
| Attribute | Purpose |
|---|---|
hx-post/get |
HTTP trigger |
hx-target |
Target element |
hx-swap |
Swap mode |
hx-trigger |
Trigger event |
hx-indicator |
Loading indicator |
Live search
templ SearchPage() {
<input type="search" hx-get="/search" hx-trigger="keyup changed delay:300ms"
hx-target="#results" hx-indicator="#spinner"/>
<div id="spinner" class="htmx-indicator">Loading...</div>
<div id="results"></div>
}
func searchHandler(w http.ResponseWriter, r *http.Request) {
views.SearchResults(searchDB(r.URL.Query().Get("q"))).Render(r.Context(), w)
}
Form + inline validation
templ CreateForm(errors map[string]string) {
<form hx-post="/create" hx-target="this" hx-swap="outerHTML">
<input name="title"/> if errors["title"] != "" { <span class="error">{ errors["title"] }</span> }
<button>Create</button>
</form>
}
On fail, same form returned with errors. hx-target="this" replaces in-place.
Blog CRUD
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /posts", listPosts)
mux.HandleFunc("POST /posts", createPost)
mux.HandleFunc("DELETE /posts/{id}", deletePost)
http.ListenAndServe(":8080", mux)
}
Delete — zero JS
<button hx-delete="/posts/1" hx-target="#post-1" hx-swap="delete">Delete</button>
Handler returns 200 OK — element removed by htmx.
SSE real-time
templ LiveFeed() { <div hx-sse="connect:/events"><div hx-sse="swap:message">Waiting...</div></div> }
No Socket.IO needed.
vs React SPA
| Dimension | templ+htmx | React SPA |
|---|---|---|
| First paint | Server HTML | Needs SSR |
| SEO | Perfect | Extra config |
| Interactions | HTML attrs | JSX+hooks |
| State | Server | Client |
| Build | templ generate+go build |
Vite+TS |
| Deploy | Single binary ~15MB | Static+Node |
| Cold start | ms | SSR-dependent |
| Team | Go only | Frontend needed |
Core: templ+htmx = Hypermedia-Driven (server renders, client swaps). React = Client-Side.
Deployment
templ generate && go build -o myapp . && ./myapp
Perf: response caching, static via nginx/CDN, DB pooling.
FAQ
Q1: File upload? — hx-encoding="multipart/form-data".
Q2: Drag-and-drop? — Alpine.js (~15KB).
Q3: Slow with many components? — 200+ ~1.5s on M3 Pro.
Q4: Frontend collab? — Give views/. templ ~ JSX.
Conclusion
Different paradigm: state on server, interactions in HTML. "Display+forms+search" uses half the code.
Learning path: counter → hx-post → form validation → search → deploy.