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
.templ source and nowhere else. Every bypass carries a named API, and reaching for
one is a decision to emit unchecked output.
.templ source and the generated *_templ.go are one artifact. Neither is correct without the other.
Syntax
{ } writes a value; {{ }} runs Go statements. html/template gives {{ }} the opposite job, and that habit
carries over silently.
{ } accepts string, the numeric types, bool, and any type defined on them. Convert anything else with
fmt.Sprintf first.
- An expression returning
(T, error) propagates its error out of Render, 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, or switch parses as a statement. Wrap it as an expression — { "if you must" }
— or capitalize the keyword.
- A comment inside a
templ block is an HTML comment and reaches the output. // and /* */ work only outside a
templ block, 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.Buffer and 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, because
templ.ComponentFunc writes 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.WithFragments and templ.RenderFragments execute 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 with templ.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 Go bool: <input disabled?={ locked }/> emits the attribute or omits
it.
- Spread a
templ.Attributes map with { attrs... }. A bool value renders the bare name or nothing, and
templ.KeyValue[string, bool] renders name="value" only when the bool is true.
- An attribute whose key comes from an expression is handled as a plain string. An
href or an on* handler built
that way gets no URL sanitization and no JavaScript handling.
- Never mutate a package-level
templ.Attributes value. It is one map shared by every concurrent render. Build a
fresh templ.Attributes{} per call, or copy the package-level one with maps.Clone before 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. Pass templ.Raw only markup the program itself produced.
- Only
href, src, and action sanitize a dynamic URL. Wrap a URL bound to any other attribute — an htmx
hx-get, for example — in templ.URL().
- A constant URL is never sanitized.
<a href="javascript:alert(1)"> written literally reaches the output
unchanged.
templ.SafeURL, templ.SafeCSS, templ.SafeCSSProperty, and templ.JSExpression each switch a sanitizer off.
Pass them a compile-time constant, or a value the program built from data it owns.
templ.JSUnsafeFuncCall skips function-name sanitization. Never build its function name from request data.
- A sanitizer substitutes a fixed marker instead of failing.
about:invalid#TemplFailedSanitizationURL for a URL,
zTemplUnsafeCSSPropertyName and zTemplUnsafeCSSPropertyValue for CSS, __templ_invalid_function_name for a
JavaScript function name. A marker in the output names the sanitizer that fired.
Styling
class takes a list of values: class={ "btn", templ.KV("btn-active", active) }. templ.KV contributes 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
css block 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
css block renders its <style> tag once per unique class per request; a raw <style> element renders every
time.
- A
css block 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 an on* attribute value, or as a
standalone <script> element when called with @.
templ.JSExpression passes event or this into templ.JSFuncCall and emits its string verbatim.
- Load a shared function once through a package-level
templ.OnceHandle, bind per-instance data through data-*
attributes, and wrap the setup in an IIFE that finds its own element through document.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.
ctx is implicit in every component and carries whatever the Render call 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 generate after every .templ edit, before building, testing, or linting. A stale *_templ.go
compiles cleanly and renders the previous markup.
- Commit every generated
*_templ.go. The Go build reads it and never runs templ.
templ fmt formats .templ files. gofmt and golangci-lint fmt do not read them.
templ generate -watch is a development loop. Run a plain templ generate before 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.Diff against 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.
1---2name: templ3description: Write and review templ templates: syntax, components, attributes, styling, JavaScript data passing, escaping and sanitization, code generation, and testing.4---56A templ template is a rendering function, not a program. Three biases decide most calls:78- **Compute in Go, render in templ.** Every transformation, validation, and I/O call belongs in a Go function that hands9 the template a finished value.10- **Escaping is automatic inside `.templ` source and nowhere else.** Every bypass carries a named API, and reaching for11 one is a decision to emit unchecked output.12- **`.templ` source and the generated `*_templ.go` are one artifact.** Neither is correct without the other.1314## Syntax1516- **`{ }` writes a value; `{{ }}` runs Go statements.** `html/template` gives `{{ }}` the opposite job, and that habit17 carries over silently.18- **`{ }` accepts `string`, the numeric types, `bool`, and any type defined on them.** Convert anything else with19 `fmt.Sprintf` first.20- **An expression returning `(T, error)` propagates its error out of `Render`**, with the source location attached.21- **Close every tag in the source, void elements included** — `<br/>`, `<img src="x"/>`. templ strips the `/` from void22 elements in the output, so the source form and the rendered form differ by design.23- **Text starting with `if`, `for`, or `switch` parses as a statement.** Wrap it as an expression — `{ "if you must" }`24 — or capitalize the keyword.25- **A comment inside a `templ` block is an HTML comment and reaches the output.** `//` and `/* */` work only outside a26 `templ` block, and HTML comments do not nest.27- **Bind an expensive call to a `{{ }}` variable** rather than repeating it across two `{ }` expressions.2829Read [`${CLAUDE_SKILL_DIR}/references/syntax.md`] when `templ generate` reports a parse error, or when writing the first30template in a package — it carries the file layout, every control-flow form, and the escaping behavior with rendered31output.3233## Components3435- **A component can write part of its output before it returns an error.** Render into a `bytes.Buffer` and copy on36 success where the response must be all-or-nothing.37- **A code-only component escapes nothing on its own.** Wrap every interpolated value in `templ.EscapeString`, because38 `templ.ComponentFunc` writes exactly the bytes handed to it.39- **Declare every `templ.NewOnceHandle()` at package level.** `@templ.NewOnceHandle().Once()` written inline builds a40 fresh handle per call, so the guarded content renders every time.41- **Wrap a once-guarded asset shared across packages in an exported component.** The handle is package state and cannot42 be shared any other way.43- **A fragment suppresses output, not work.** `templ.WithFragments` and `templ.RenderFragments` execute the whole44 template and write only the named fragment.45- **`{ children... }` renders the block a caller passed as `@component() { ... }`.** A code-only component reads the46 same block from the context with `templ.GetChildren`.4748Read [`${CLAUDE_SKILL_DIR}/references/components.md`] when composing components, passing one as a parameter, or49rendering a fragment — it carries the composition forms, the method-component and code-only shapes, the children context50API, and the fragment API.5152## Attributes5354- **A conditional attribute replaces the earlier attribute of the same name.** Values do not merge, so every branch55 repeats the base value.56- **`?=` renders a boolean attribute from a Go `bool`**: `<input disabled?={ locked }/>` emits the attribute or omits57 it.58- **Spread a `templ.Attributes` map with `{ attrs... }`.** A `bool` value renders the bare name or nothing, and59 `templ.KeyValue[string, bool]` renders `name="value"` only when the bool is true.60- **An attribute whose key comes from an expression is handled as a plain string.** An `href` or an `on*` handler built61 that way gets no URL sanitization and no JavaScript handling.62- **Never mutate a package-level `templ.Attributes` value.** It is one map shared by every concurrent render. Build a63 fresh `templ.Attributes{}` per call, or copy the package-level one with `maps.Clone` before changing it.6465Read [`${CLAUDE_SKILL_DIR}/references/attributes.md`] when an attribute value or key is computed, or when a spread map66renders in an unexpected shape — it carries every attribute form with its rendered output.6768## Escaping and Sanitization6970- **`{ }` HTML-escapes its value; `@templ.Raw()` does not.** Pass `templ.Raw` only markup the program itself produced.71- **Only `href`, `src`, and `action` sanitize a dynamic URL.** Wrap a URL bound to any other attribute — an htmx72 `hx-get`, for example — in `templ.URL()`.73- **A constant URL is never sanitized.** `<a href="javascript:alert(1)">` written literally reaches the output74 unchanged.75- **`templ.SafeURL`, `templ.SafeCSS`, `templ.SafeCSSProperty`, and `templ.JSExpression` each switch a sanitizer off.**76 Pass them a compile-time constant, or a value the program built from data it owns.77- **`templ.JSUnsafeFuncCall` skips function-name sanitization.** Never build its function name from request data.78- **A sanitizer substitutes a fixed marker instead of failing.** `about:invalid#TemplFailedSanitizationURL` for a URL,79 `zTemplUnsafeCSSPropertyName` and `zTemplUnsafeCSSPropertyValue` for CSS, `__templ_invalid_function_name` for a80 JavaScript function name. A marker in the output names the sanitizer that fired.8182## Styling8384- **`class` takes a list of values**: `class={ "btn", templ.KV("btn-active", active) }`. `templ.KV` contributes its85 class when the bool is true.86- **Compute a class string in a `{{ }}` block only where the logic exceeds a set of independent toggles.**87- **A `css` block generates a hash-suffixed class name.** Pass the function result — `class={ primaryButton() }` — and88 never write the generated name into a stylesheet, a test selector, or client JavaScript.89- **A `css` block renders its `<style>` tag once per unique class per request; a raw `<style>` element renders every90 time.**91- **A `css` block taking arguments generates one class per distinct argument set.** Never call one with a value drawn92 from an unbounded range.9394Read [`${CLAUDE_SKILL_DIR}/references/styling.md`] when a conditional class needs a form other than `templ.KV`, when the95`style` attribute takes a computed value, or when serving templ CSS as one stylesheet — it carries every class and style96value type, the CSS-component argument forms, and the `templ.NewCSSMiddleware` setup.9798## JavaScript99100- **Pass data as an attribute or a JSON script, never as interpolated code.** `data-config={ templ.JSONString(cfg) }`101 carries component-scoped data; `@templ.JSONScript("id", cfg)` carries page-level data.102- **`templ.JSFuncCall("fn", args...)` JSON-encodes its arguments.** It renders as an `on*` attribute value, or as a103 standalone `<script>` element when called with `@`.104- **`templ.JSExpression` passes `event` or `this` into `templ.JSFuncCall`** and emits its string verbatim.105- **Load a shared function once through a package-level `templ.OnceHandle`, bind per-instance data through `data-*`106 attributes, and wrap the setup in an IIFE** that finds its own element through `document.currentScript`.107108Read [`${CLAUDE_SKILL_DIR}/references/javascript.md`] when wiring client behavior to a component — it carries the109rendered output of each API, the once-handle plus `data-*` plus IIFE pattern in full, and the bundling route for110TypeScript.111112## View Models and Context113114- **Give each template a view model built by a constructor in Go.** The template reads fields and ranges over slices; it115 opens no database, calls no service, and formats nothing Go could have formatted.116- **Test the constructor as ordinary Go.** A view model needs no rendering to be verified.117- **`ctx` is implicit in every component** and carries whatever the `Render` call carried.118- **Reserve the context for cross-cutting values** — the authenticated user, the locale, the theme — set by HTTP119 middleware and read through a getter returning `(T, bool)`. Pass everything else as a parameter, because a missing120 context value fails at render time and a missing parameter fails at compile time.121122Read [`${CLAUDE_SKILL_DIR}/references/patterns.md`] when building a layout, wiring a context value through middleware,123or embedding `html/template` output — it carries the view-model, nested-layout, multi-slot, context-helper, and interop124shapes.125126## Toolchain127128- **Run `templ generate` after every `.templ` edit, before building, testing, or linting.** A stale `*_templ.go`129 compiles cleanly and renders the previous markup.130- **Commit every generated `*_templ.go`.** The Go build reads it and never runs templ.131- **`templ fmt` formats `.templ` files.** `gofmt` and `golangci-lint fmt` do not read them.132- **`templ generate -watch` is a development loop.** Run a plain `templ generate` before building a release artifact.133134## Testing135136- **Parse the rendered HTML; never assert on the string.** templ minifies its output, so whitespace and attribute order137 are not a contract.138- **Select with `data-testid`**, never with a class name or the document structure.139- **Compare a whole page with `htmldiff.Diff`** against an embedded fixture rather than with string equality.140- **Test a page for the presence of its child components**, and test each component's own rendering in its own test.141142Read [`${CLAUDE_SKILL_DIR}/references/testing.md`] when writing the first test for a component or a page — it carries143the render-and-parse harness, the `htmldiff` snapshot form, and how to render a fragment under test.144145## Application146147When **writing** templ, apply these conventions silently — do not narrate a rule while following it. Where existing code148contradicts one, follow the codebase and flag the divergence once. Run `templ generate` before reporting that the change149builds.150151When **reviewing** templ, cite the violation and show the fix inline. Do not lecture.152153```154Bad: "templ only sanitizes href, src, and action, so htmx attributes should be wrapped..."155Good: hx-get={ path } -> hx-get={ templ.URL(path) }156```157158## Integration159160The **golang** skill governs every line outside a `templ` block — naming, error handling, testing conventions, and the161Go toolchain — and wins on any question of how the Go code reads. This skill governs `.templ` source and the templ162runtime API. The **coding** skill governs workflow. All are active at once.163164**When a template needs to decide something, move the decision into Go.**