A templ template is a rendering function, not a program. Three biases decide most calls:
- Compute in Go, render in templ. Every transformation, validation, and I/O call belongs in a Go function that hands the template a finished value.
- Escaping is automatic inside
.templsource and nowhere else. Every bypass carries a named API, and reaching for one is a decision to emit unchecked output. .templsource and the generated*_templ.goare one artifact. Neither is correct without the other.
Syntax
{ }writes a value;{{ }}runs Go statements.html/templategives{{ }}the opposite job, and that habit carries over silently.{ }acceptsstring, the numeric types,bool, and any type defined on them. Convert anything else withfmt.Sprintffirst.- An expression returning
(T, error)propagates its error out ofRender, with the source location attached. - Close every tag in the source, void elements included —
<br/>,<img src="x"/>. templ strips the/from void elements in the output, so the source form and the rendered form differ by design. - Text starting with
if,for, orswitchparses as a statement. Wrap it as an expression —{ "if you must" }— or capitalize the keyword. - A comment inside a
templblock is an HTML comment and reaches the output.//and/* */work only outside atemplblock, and HTML comments do not nest. - Bind an expensive call to a
{{ }}variable rather than repeating it across two{ }expressions.
Read [${CLAUDE_SKILL_DIR}/references/syntax.md] when templ generate reports a parse error, or when writing the first
template in a package — it carries the file layout, every control-flow form, and the escaping behavior with rendered
output.
Components
- A component can write part of its output before it returns an error. Render into a
bytes.Bufferand copy on success where the response must be all-or-nothing. - A code-only component escapes nothing on its own. Wrap every interpolated value in
templ.EscapeString, becausetempl.ComponentFuncwrites exactly the bytes handed to it. - Declare every
templ.NewOnceHandle()at package level.@templ.NewOnceHandle().Once()written inline builds a fresh handle per call, so the guarded content renders every time. - Wrap a once-guarded asset shared across packages in an exported component. The handle is package state and cannot be shared any other way.
- A fragment suppresses output, not work.
templ.WithFragmentsandtempl.RenderFragmentsexecute the whole template and write only the named fragment. { children... }renders the block a caller passed as@component() { ... }. A code-only component reads the same block from the context withtempl.GetChildren.
Read [${CLAUDE_SKILL_DIR}/references/components.md] when composing components, passing one as a parameter, or
rendering a fragment — it carries the composition forms, the method-component and code-only shapes, the children context
API, and the fragment API.
Attributes
- A conditional attribute replaces the earlier attribute of the same name. Values do not merge, so every branch repeats the base value.
?=renders a boolean attribute from a Gobool:<input disabled?={ locked }/>emits the attribute or omits it.- Spread a
templ.Attributesmap with{ attrs... }. Aboolvalue renders the bare name or nothing, andtempl.KeyValue[string, bool]rendersname="value"only when the bool is true. - An attribute whose key comes from an expression is handled as a plain string. An
hrefor anon*handler built that way gets no URL sanitization and no JavaScript handling. - Never mutate a package-level
templ.Attributesvalue. It is one map shared by every concurrent render. Build a freshtempl.Attributes{}per call, or copy the package-level one withmaps.Clonebefore changing it.
Read [${CLAUDE_SKILL_DIR}/references/attributes.md] when an attribute value or key is computed, or when a spread map
renders in an unexpected shape — it carries every attribute form with its rendered output.
Escaping and Sanitization
{ }HTML-escapes its value;@templ.Raw()does not. Passtempl.Rawonly markup the program itself produced.- Only
href,src, andactionsanitize a dynamic URL. Wrap a URL bound to any other attribute — an htmxhx-get, for example — intempl.URL(). - A constant URL is never sanitized.
<a href="javascript:alert(1)">written literally reaches the output unchanged. templ.SafeURL,templ.SafeCSS,templ.SafeCSSProperty, andtempl.JSExpressioneach switch a sanitizer off. Pass them a compile-time constant, or a value the program built from data it owns.templ.JSUnsafeFuncCallskips function-name sanitization. Never build its function name from request data.- A sanitizer substitutes a fixed marker instead of failing.
about:invalid#TemplFailedSanitizationURLfor a URL,zTemplUnsafeCSSPropertyNameandzTemplUnsafeCSSPropertyValuefor CSS,__templ_invalid_function_namefor a JavaScript function name. A marker in the output names the sanitizer that fired.
Styling
classtakes a list of values:class={ "btn", templ.KV("btn-active", active) }.templ.KVcontributes its class when the bool is true.- Compute a class string in a
{{ }}block only where the logic exceeds a set of independent toggles. - A
cssblock generates a hash-suffixed class name. Pass the function result —class={ primaryButton() }— and never write the generated name into a stylesheet, a test selector, or client JavaScript. - A
cssblock renders its<style>tag once per unique class per request; a raw<style>element renders every time. - A
cssblock taking arguments generates one class per distinct argument set. Never call one with a value drawn from an unbounded range.
Read [${CLAUDE_SKILL_DIR}/references/styling.md] when a conditional class needs a form other than templ.KV, when the
style attribute takes a computed value, or when serving templ CSS as one stylesheet — it carries every class and style
value type, the CSS-component argument forms, and the templ.NewCSSMiddleware setup.
JavaScript
- Pass data as an attribute or a JSON script, never as interpolated code.
data-config={ templ.JSONString(cfg) }carries component-scoped data;@templ.JSONScript("id", cfg)carries page-level data. templ.JSFuncCall("fn", args...)JSON-encodes its arguments. It renders as anon*attribute value, or as a standalone<script>element when called with@.templ.JSExpressionpasseseventorthisintotempl.JSFuncCalland emits its string verbatim.- Load a shared function once through a package-level
templ.OnceHandle, bind per-instance data throughdata-*attributes, and wrap the setup in an IIFE that finds its own element throughdocument.currentScript.
Read [${CLAUDE_SKILL_DIR}/references/javascript.md] when wiring client behavior to a component — it carries the
rendered output of each API, the once-handle plus data-* plus IIFE pattern in full, and the bundling route for
TypeScript.
View Models and Context
- Give each template a view model built by a constructor in Go. The template reads fields and ranges over slices; it opens no database, calls no service, and formats nothing Go could have formatted.
- Test the constructor as ordinary Go. A view model needs no rendering to be verified.
ctxis implicit in every component and carries whatever theRendercall carried.- Reserve the context for cross-cutting values — the authenticated user, the locale, the theme — set by HTTP
middleware and read through a getter returning
(T, bool). Pass everything else as a parameter, because a missing context value fails at render time and a missing parameter fails at compile time.
Read [${CLAUDE_SKILL_DIR}/references/patterns.md] when building a layout, wiring a context value through middleware,
or embedding html/template output — it carries the view-model, nested-layout, multi-slot, context-helper, and interop
shapes.
Toolchain
- Run
templ generateafter every.templedit, before building, testing, or linting. A stale*_templ.gocompiles cleanly and renders the previous markup. - Commit every generated
*_templ.go. The Go build reads it and never runs templ. templ fmtformats.templfiles.gofmtandgolangci-lint fmtdo not read them.templ generate -watchis a development loop. Run a plaintempl generatebefore building a release artifact.
Testing
- Parse the rendered HTML; never assert on the string. templ minifies its output, so whitespace and attribute order are not a contract.
- Select with
data-testid, never with a class name or the document structure. - Compare a whole page with
htmldiff.Diffagainst an embedded fixture rather than with string equality. - Test a page for the presence of its child components, and test each component's own rendering in its own test.
Read [${CLAUDE_SKILL_DIR}/references/testing.md] when writing the first test for a component or a page — it carries
the render-and-parse harness, the htmldiff snapshot form, and how to render a fragment under test.
Application
When writing templ, apply these conventions silently — do not narrate a rule while following it. Where existing code
contradicts one, follow the codebase and flag the divergence once. Run templ generate before reporting that the change
builds.
When reviewing templ, cite the violation and show the fix inline. Do not lecture.
Bad: "templ only sanitizes href, src, and action, so htmx attributes should be wrapped..."
Good: hx-get={ path } -> hx-get={ templ.URL(path) }
Integration
The golang skill governs every line outside a templ block — naming, error handling, testing conventions, and the
Go toolchain — and wins on any question of how the Go code reads. This skill governs .templ source and the templ
runtime API. The coding skill governs workflow. All are active at once.
When a template needs to decide something, move the decision into Go.