Template Syntax
Waltzing templates are simple, expressive, and based on Rust. A Waltzing template is a text file — HTML with a sprinkling of @ — that the compiler turns into a plain Rust function at build time. If you know Rust, you already know most of the syntax; the rest fits on this page.
Overview
A template lives in a .wtz file under your templates/ directory. Each @fn becomes a callable Rust function that returns rendered Content. Because it compiles to Rust, its parameters are typed and its expressions are checked by the compiler — a mistake is a build error, not a runtime surprise.
@* templates/pages/hello.wtz *@
@use crate::models::User
@fn page(user: &User) {
<!DOCTYPE html>
<html>
<body>
<h1>Hello, @user.name</h1>
@if user.returning {
<p>Welcome back!</p>
}
</body>
</html>
} The @ character
The single special character is @. It marks the boundary between static HTML and dynamic content: whatever follows is a Rust expression. The compiler is smart about where the expression ends, so most of the time you can just write @name. Wrap anything ambiguous in parentheses, and write @@ for a literal @.
@name @* interpolate a value (auto-escaped) *@
@user.email @* field access and method calls *@
@(price * quantity) @* wrap a complex expression in parens *@
@@media @* a literal '@' — write it twice *@
<a href=@"mailto:me@x.com"> @* @"..." is a literal string *@ Template parameters
Every @fn declares typed parameters — the template's inputs. Parameters can have default values, so callers only pass what they need. The conventional entry point is named apply.
@fn button(label: &str, kind: &str = "primary", disabled: bool = false) {
<button class=@format("btn btn-{}", kind) @if disabled { disabled }>
@label
</button>
}
@* Call it — defaults fill in the rest: *@
<@button label="Save" />
<@button label="Delete" kind="danger" disabled /> Expressions
Anything after @ is a Rust expression: field access, method calls, arithmetic, iterators. Keep simple expressions bare; parenthesize compound ones with @(…) so the parser knows where they end.
@fn stats(items: &[Item]) {
<p>@items.len() total</p>
<p>@items.iter().filter(|i| i.active).count() active</p>
<p>Newest: @items.last().map(|i| i.name).unwrap_or("—")</p>
} Escaping & safety
Interpolated text is HTML-escaped automatically — <, > and & become entities, so untrusted data can't break out of the page. For the sharp edges, safe_url rejects dangerous URI schemes, json encodes data for scripts and attributes, and safe lets you opt out when you have HTML you trust.
@fn note(text: &str, link: &str, meta: &Meta, trusted_html: &str) {
<p>@text</p> @* < > & are auto-escaped *@
<a href=@safe_url(link)>more</a> @* blocks javascript:/data: URIs *@
<span data-meta=@json(meta)></span>
@safe(trusted_html) @* opt out only when you mean it *@
} Iterating
Use @for to loop, exactly like Rust's for:
@fn list(users: &[User]) {
<ul>
@for user in users {
<li>@user.name — @user.email</li>
}
</ul>
} Conditionals
@if / else if / else work as you'd expect, and support if let for pattern binding:
@fn status(code: u16) {
@if code < 300 {
<span class="ok">OK</span>
} else if code < 500 {
<span class="warn">Client error</span>
} else {
<span class="err">Server error</span>
}
} Pattern matching
@match brings Rust's full pattern matching into templates — including guards and bindings. Each arm's body is template content.
@fn badge(role: &Role) {
@match role {
Role::Admin => { <b>Admin</b> }
Role::Member if role.verified() => { <span>Member ✓</span> }
_ => {}
}
} Reusable blocks (content slots)
A parameter of type Content is a slot: callers pass a block of template content into it. This is how layouts and wrappers work — the same idea as children in a component.
@* Declare a component with a content slot: *@
@fn card(title: &str, body: Content) {
<section class="card">
<h2>@title</h2>
@body
</section>
}
@* Fill the slot when you call it: *@
@fn page() {
<@card title="Notes">
<p>Anything can go in the body.</p>
</@>
} Reusable values
Use @let to name a value and reuse it. It's an ordinary Rust binding, evaluated once where it appears.
@fn total(cart: &Cart) {
@let subtotal = cart.items.iter().map(|i| i.price).sum::<i64>();
@let tax = subtotal * 8 / 100;
<p>Subtotal: @currency(subtotal as f64 / 100.0, "USD")</p>
<p>Tax: @currency(tax as f64 / 100.0, "USD")</p>
<p>Total: @currency((subtotal + tax) as f64 / 100.0, "USD")</p>
} Imports
@import pulls in another template so you can call it; @use brings Rust types and functions into scope, just like a Rust use.
@import /components/card.wtz as card @* another template, aliased *@
@import /components/nav.wtz @* imports `nav` *@
@use crate::models::{User, Post} @* bring Rust types into scope *@
@use std::collections::HashMap Function tags (components)
Call one template from another with an HTML-like function tag. Attributes can be string literals, @expressions, or bare boolean flags; a tag with a body passes that body into the callee's Content slot. When the function is named apply, you can drop it from the tag.
@* Self-closing *@
<@icon name="star" />
@* With a content slot (</@> closes it) *@
<@modal title="Confirm">
<p>Are you sure?</p>
</@>
@* Attributes: string literals, @expressions, and bare bool flags *@
<@field label="Email" value=@user.email required /> Comments
@* … *@ is a template comment — removed at compile time and never sent to the browser. Add asterisks to nest.
@* A single-line comment — stripped at compile time *@
@**
A block comment. Add asterisks to nest:
@* like this inner comment *@
**@ Embedded languages
Waltzing understands the languages inside your page. <style>, <script> and @```json blocks are parsed as CSS, JavaScript and JSON — with template interpolation inside them. It pairs especially well with htmx and Alpine.js.
@fn widget(config: &Config) {
<style>
.widget { color: @config.color; }
</style>
<div x-data=@```json { open: false, url: @config.url } ```@>
<button x-on:click="open = !open">Toggle</button>
</div>
<script>
console.log("widget mounted");
</script>
} Ready to build something?