DocsBuilding pages
Rendering & components
html! is parsed at compile time, escapes what it interpolates, and knows which expressions belong to the server and which to the browser. Components are ordinary Rust functions you can also write as tags.
The ownership split
Rahti has two expression dialects, and they never interchange.
| Syntax | Owner | Evaluated |
|---|---|---|
| @{rust_expression} | Rust, through Rahti | On the server, while the page renders. |
| {javascript_expression} | PulsePoint | In the browser, whenever its state changes. |
html! {
<section>
<h1>@{server_title}</h1>
<button onclick={setCount(count + 1)}>{count}</button>
<script>
const [count, setCount] = pp.state(@{initial_count});
</script>
</section>
}They also print differently. @{…} renders a Rust value through Display, so false renders as false. A {…} binding serializes its result the way React serializes a JSX child, so a boolean or a None seeded into the browser renders as nothing at all — a correct value with no visible output. The full table is under value rendering; read it before binding anything that is not a string or a number.
Text, roots, and attributes
Author text as quoted Rust strings. A text run is exactly one string literal, so break a long one with a backslash rather than writing two.
// Authored text is a quoted Rust string. Plain HTML-like text is accepted,
// but Rust tokenization makes punctuation and whitespace fragile.
<p>"Stable authored text"</p>
// A run is exactly one string literal. Break a long line with a backslash:
<p>
"One paragraph, written across several source lines, because the \
backslash swallows the newline and the indentation after it."
</p>An html! block produces one rooted markup value: a literal element, a single component tag, or a <>…</> fragment. A document doctype may precede the document root. Void elements are self-closing in authored markup.
<a href=@{url}>@{label}</a>
<input value={name} oninput={setName(target.value)} />Rahti quotes dynamic attributes in the emitted HTML, and client expressions stay present for PulsePoint to bind. <script> and <style> bodies are raw text: browser braces stay literal. @{…} works inside a script — it renders through RenderJs — and is not lifted inside a style.
Escaping and trusted markup
Rust strings and values are escaped in text and in attribute position; attributes also escape quotes. An Html value is already trusted and passes through untouched.
// Escaped on the way out — the default, and almost always what you want.
<p>@{user_supplied}</p>
// Trusted markup, rendered as written. Only for markup you produced or
// sanitised yourself.
<div>@{Html::from_raw(rendered_markdown)}</div>
// Other constructors.
Html::escape("<b>not bold</b>"); // safe text
Html::empty(); // no output at all
Html::concat(parts); // several fragments, in orderInside a <script>, primitives render as JavaScript literals, strings are JSON-quoted, None is null, and Json(&value) embeds objects and arrays. Rahti neutralises < in serialized script values so a value cannot close the script around it.
// To show a PulsePoint brace as text, write the character reference. The
// macro protects it, so the runtime renders a brace instead of binding.
<p>"{user}"</p>Components
A component is an ordinary PascalCase Rust function. A tag name starting with a capital letter is a component — no HTML tag does — and its attributes name the function's parameters.
use crate::rahti::{Html, component, html};
#[component]
pub fn Card(title: &str, children: Html) -> Html {
html! {
<section>
<h2>@{title}</h2>
<slot />
</section>
}
}<Card title="Profile">
<p>"Ada"</p>
</Card>A prop value is a Rust expression: a literal as written (title="Profile", count=3), or @{expr} for anything computed. A bare prop is the boolean shorthand — <Card wide> passes wide: true. A client {…} binding is not a prop value and fails to compile: a component runs at render, and there is nothing of the browser in it.
// The tag form is compile-time sugar over the function call, and the call
// form is what expression contexts use — a loop, a condition, a map.
@{Html::concat(users.iter().map(|user| Card(user, Html::empty())))}Prop mistakes fail with rustc's ordinary struct-literal errors: a missing prop is "missing field", an unknown one is "no field named", a doubled one is "specified more than once". Behind the tag, #[component] generates a hidden props struct sharing the function's name, and the one use that imports the component imports both.
The rules:
- use
#[component]with no arguments; - name the function in PascalCase — the capital is what makes a tag resolve to it;
- return
Html, and end branches and returns in thehtml!value; - take props as normal typed Rust arguments;
- keep one root — a literal element, a single component tag, or a fragment;
- do not stamp
pp-componentby hand.
Composition
A block may render a component tag as its whole output. A block rooted in a component tag owns no element of its own, so it takes no boundary marker and adds no reactive scope: it dissolves into the component it renders, exactly as a React wrapper that returns another component contributes no DOM.
#[component]
pub fn Spotlight(children: Html) -> Html {
html! {
<Card title="Spotlight">@{children}</Card>
}
}- a wrapper that only forwards props and children needs no element around the inner tag;
- a component that carries its own
<script>keeps an element root — or a fragment root, which is one — since a dissolved wrapper has no block for the script to live in; - a root layout keeps its literal
<html>root: a document's doctype and document element are written in the layout, with components inside them.
Fragments
A block that renders siblings may root itself in <>…</>.
#[component]
pub fn Pair(label: &str) -> Html {
html! {
<>
<dt>@{label}</dt>
<dd><slot /></dd>
</>
}
}<!--pp:pair_3f2a91c8--><dt>Role</dt><dd>Engineer</dd><!--/pp-->Comments are legal in every content context, so the served markup stays valid wherever the fragment lands — <tbody>, <ul>, <select> included — and a page without JavaScript renders the siblings bare, exactly as written. At hydration the runtime turns the pair into a live, layout-invisible boundary: the siblings participate in the parent's flex, grid or flow as direct items, and the block keeps its own state like any other.
After hydration the boundary is an element in the DOM, so a CSS child selector (.parent > *) sees it rather than the siblings. Prefer class selectors inside fragments.
Children and slots
Children are not implicit in the function: declare children: Html, then place <slot />. At the call site they are whatever sits between the tags — markup, quoted text, other component tags, @{…} values, or several siblings, joined in order.
// Children built in Rust are handed in with `@{…}`.
let rows = Html::concat(items.iter().map(|item| html! {
<li>@{item}</li>
}));
html! {
<Card title="Rows">
<ul>@{rows}</ul>
</Card>
}An empty tag pair passes Html::empty(). A self-closing tag passes no children argument at all: it is the form for a component that declares none, and calling one that does declare children self-closing fails with "missing field `children`". A component that takes children and passes it on interpolates it between the next component's tags instead of writing a slot.
The reactive boundary rule
Each stamped block is a PulsePoint scope. Children written between component tags are a block of their own when they have a single root element — so a client binding written there cannot read state declared by the parent page's script. This fails silently, as literal, unbound text.
// Wrong: the binding is in the children block, the state is in the page.
<Panel title="Counter">
<p>{count}</p>
</Panel>
<script>
const [count, setCount] = pp.state(0);
</script>
// Right: state, markup and script in one block — here, a component of
// its own.
#[component]
pub fn Counter() -> Html {
html! {
<div>
<p>{count}</p>
<button onclick={setCount(count + 1)}>"Add"</button>
<script>
const [count, setCount] = pp.state(0);
</script>
</div>
}
}Keep state, functions, bindings and the script that declares them in the same block. A reusable interactive child should be its own #[component], or carry its own script inside its own root. A child fragment with several roots, or none, is not stamped — which is the escape hatch when several siblings must share one scope.
Component modules and libraries
Application components live under src/components/, nested directories included. File and directory names must be valid, non-keyword Rust module identifiers: no leading digit, no hyphens. Dotfiles and non-Rust files are ignored, and the build regenerates the module files on every build.
src/components/card.rs -> crate::components::card::Card
src/components/forms/field.rs -> crate::components::forms::field::FieldOne file may export several components. External component libraries depend on rahti, use the same macros, and may ship a package-root ui.css. The build detects them through dependency metadata, includes their Rust sources in Tailwind scanning, appends their CSS, and registers their component RPCs through the link-time registry.
