Skip to content

templ

HTML templates that are compiled rather than parsed. You write .templ files, a generator produces .go, and a typo in a field name becomes a compile error instead of a blank page.

Module: github.com/a-h/templ, plus the templ CLI.

Compare with templates, which does the same job with html/template.

templ itemRow(i Item) {
    <li class="row">
        <span>{ i.Name }</span>
    </li>
}

The pitch

html/template resolves {{.Name}} at runtime. Rename the field and you find out when the page renders — or when that branch renders, which may be in production.

templ generates Go. i.Name is a real field access, so the compiler checks it, your editor completes it, and renaming with a refactoring tool updates the template.

The syntax

A templ block is a function returning a component:

package main

import "fmt"

templ itemRow(i Item) {
    <li class="row">
        <span>{ i.Name }</span>
        <span>{ fmt.Sprintf("$%.2f", i.Price) }</span>
        if len(i.Tags) > 0 {
            <em>{ fmt.Sprint(len(i.Tags)) } tags</em>
        } else {
            <em>untagged</em>
        }
    </li>
}

Braces interpolate a Go expression returning a string. Not a template language — real Go, with real imports. That is why the price formatting is fmt.Sprintf rather than a custom template function.

if, for and switch are Go keywords with Go syntax, so there is no second dialect to learn and no {{end}} to forget.

for _, i := range items {
    @itemRow(i)
}

@name(args) renders another component.

Composition with children...

A layout takes the page content as children:

templ layout(title string) {
    <html>
        <head><title>{ title }</title></head>
        <body>
            { children... }
        </body>
    </html>
}

templ ItemList(title string, items []Item) {
    @layout(title) {
        <ul>
            for _, i := range items {
                @itemRow(i)
            }
        </ul>
    }
}

The block after @layout(title) becomes its children. This replaces define/block from html/template, and it composes like ordinary function calls.

Generating

templ generate

Produces page_templ.go next to each .templ, headed:

// Code generated by templ - DO NOT EDIT.

Commit the generated files. That way the repo builds with plain go build and CI does not need the CLI — the reasoning from build and codegen. Wire templ generate into your Makefile, and have CI regenerate and fail on a diff so nobody forgets.

templ generate --watch regenerates on save during development.

Rendering

A component renders to any io.Writer:

err := ItemList("Shop & Co", items).Render(context.Background(), w)

Which in a net/http handler is:

func (h Handler) list(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    if err := ItemList("Shop", items).Render(r.Context(), w); err != nil {
        http.Error(w, "render failed", http.StatusInternalServerError)
    }
}

There is a catch worth knowing: Render streams, so it may have written part of the page before failing — and the status is already 200. If that matters, render into a bytes.Buffer first and copy it out only on success.

Escaping

Contextual, like html/template:

<title>Shop &amp; Co</title>
<span>Widget &lt;b&gt;</span>
<a href="/x?q=1&amp;b=2">link</a>

Text is escaped, attributes are escaped, and templ.URL sanitises a URL in an href. A dangerous scheme is replaced outright:

templ LinkTo(u string) {
    <a href={ templ.URL(u) }>link</a>
}
"/x?q=1&b=2"                    <a href="/x?q=1&amp;b=2">link</a>
"https://example.com/a"         <a href="https://example.com/a">link</a>
"mailto:a@b.c"                  <a href="mailto:a@b.c">link</a>
"javascript:alert(1)"           <a href="about:invalid#TemplFailedSanitizationURL">link</a>
"data:text/html,<script>1"      <a href="about:invalid#TemplFailedSanitizationURL">link</a>

about:invalid#TemplFailedSanitizationURL is templ's equivalent of html/template's ZgotmplZ marker — finding it in a rendered page means unsafe content reached a URL position, and the fix is in the data rather than the template.

templ.Raw opts out of escaping and should only ever see content you produced.

Because escaping happens at generation time in known positions, it is harder to accidentally bypass than in a string-based template.

View models

Do not hand your database models to a template. A dedicated view type keeps display logic out of both:

type ItemVM struct {
    Name      string
    PriceText string
    Badge     string
}

The handler maps domain to view, the template renders the view. Pure mapping functions are easy to test, and the template stays free of if user.Role == "admin" && !user.Suspended conditionals.

Testing

A component is a function returning something with a Render method, so tests are plain Go:

var sb strings.Builder
err := ItemList("t", items).Render(context.Background(), &sb)
// then assert on sb.String()

Assert on structure rather than exact bytes — a golden file from helpers and golden files works well here, since a whole page is too big to inline.

Costs

  • A build step. Editing HTML now means regenerating, and a stale generated file is a confusing bug.
  • Editor support is separate. There is an LSP and plugins, but it is not the out-of-the-box experience of .html.
  • The generated code is noisy in diffs. Some teams gitignore it and generate in CI instead — at the cost of go build no longer working alone.
  • It is a different language in the same file. Designers editing templates need to learn it.

Against that: a renamed field cannot silently break a page, and there is no runtime template parsing at all.

From Python: this is Jinja replaced by something closer to a JSX compiler — templates become typed functions checked at build time. The trade is the same one: type safety and editor support against a compile step and a format non-programmers find less approachable.

Quick reference

Task Form
a component templ Name(args) { ... } in a .templ file
interpolate { goExpression } — real Go, returning a string
control flow Go if, for, switch — no {{end}}
call another @other(arg)
layouts { children... } plus @layout(x) { ... }
generate templ generate, --watch while developing
commit yes — the generated _templ.go files
render .Render(ctx, w) to any io.Writer
safe URLs templ.URL(...); templ.Raw only for your own HTML
test render into a strings.Builder, assert or use a golden file

Sources