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>
}