Component
A component is any Rust struct that implements the Component trait. Components are the building blocks of Tidos: each one encapsulates its own HTML structure, styles, and logic.
The view! Macro
view! is the templating macro. Use it inside a component's to_render method to write HTML mixed with Rust expressions. All text between tags must be wrapped in {}. Attribute values use that same {} syntax: a literal, a formatted string, or any expression. A : prefix turns an attribute into a toggle that's either present or absent (like disabled), instead of being set to a string value:
1let count = 42_usize;2let name = "Alice";3let is_loading = true;45view! {6 // --- Text content ---78 // Static text9 <p>{"Hello world"}</p>1011 // Formatted, with {} placeholders12 <p>{"Hello {}, you have {} items.", name, count}</p>1314 // Any Rust expression15 <p>{count.to_string()}</p>1617 // Raw HTML (not sanitized), use with care!18 <div>@html{"<em>italic</em>"}</div>1920 // --- Attributes share the exact same content syntax ---2122 // Static value23 <span class={"rounded"}></span>2425 // Formatted value (here combined with scoped_css!)26 <span class={"rounded {}", scoped_css!("./mod.css")}></span>2728 // Any Rust expression29 <div data-count={count.to_string()}></div>3031 // Toggle attribute: present or absent, like `disabled`.32 // `:disabled` reads a bool; `disabled={true}` would render disabled="true" instead.33 <button :disabled={is_loading}>{"Save"}</button>34}
The Component Trait
Implement Component for any struct. The to_render method receives a mutable reference to the Page, so a component (and any children it renders) can inject CSS or head elements while it runs:
1use tidos::{view, Component, Page};23pub struct Alert {4 pub message: String,5 pub kind: String,6}78impl Component for Alert {9 fn to_render(&self, page: &mut Page) {10 view! {11 <div class="alert" data-kind={&self.kind}>12 <p>{&self.message}</p>13 </div>14 }15 }16}
Control Tags
Control tags are special blocks inside view! that map directly to Rust control flow.
Conditions
1let logged_in = true;2let is_admin = false;34view! {5 {#if logged_in && is_admin}6 <span>{"Admin Panel"}</span>7 {:else if logged_in}8 <span>{"Dashboard"}</span>9 {:else}10 <a href="/login">{"Sign in"}</a>11 {/if}12}
Loops
1let fruits = vec!["apple", "banana", "cherry"];23view! {4 <ul>5 {#for fruit in &fruits}6 <li>{fruit}</li>7 {/for}8 </ul>9}
Pattern Matching
1enum Status { Active, Banned, Guest }2let status = Status::Active;34view! {5 {#match status}6 {:case Status::Active}7 <span data-status="active">{"Active"}</span>8 {:case Status::Banned}9 <span data-status="banned">{"Banned"}</span>10 {:case _}11 <span data-status="guest">{"Guest"}</span>12 {/match}13}
Slots
Slots let a parent pass rendered content into a child component. A component field of type Slot<'a> receives that content and renders it with @slot{self.field}. There are two forms, and which one you use depends on how many slots the component takes and whether it has other props:
Named slots
Use these when a component has multiple slots, or a single slot alongside other attributes/props. Declare a regular Slot<'a> field and fill it from the parent with {#slot:name}...{/slot}:
1// Parent2view! {3 <Card title={"News".to_string()}>4 {#slot:body}5 <p>{"This content goes into the card body."}</p>6 {/slot}7 </Card>8}910// Child component11pub struct Card<'a> {12 pub title: String,13 pub body: Slot<'a>,14}1516impl Component for Card<'_> {17 fn to_render(&self, page: &mut Page) {18 view! {19 <div class="card">20 <h2>{&self.title}</h2>21 <div class="body">22 @slot{self.body}23 </div>24 </div>25 }26 }27}
Unnamed slots
Unnamed slot: use this only when a component takes exactly one slot and has no other props. Declare the component as a tuple struct with Slot<'a> as field 0. The parent then passes content directly as children, with no {#slot:name} wrapper:
1// Parent: content is passed straight in, no {#slot:name} wrapper2view! {3 <Card>4 <p>{"This content goes straight into the card."}</p>5 </Card>6}78// Child component: a tuple struct with Slot<'a> as field 09pub struct Card<'a>(pub Slot<'a>);1011impl Component for Card<'_> {12 fn to_render(&self, page: &mut Page) {13 view! {14 <div class="card">15 @slot{self.0}16 </div>17 }18 }19}
Default Properties
Components whose struct derives Default can use .. after the explicit props to fill all remaining fields with their default values. The struct must implement Default, either via #[derive(Default)] or manually:
1use tidos::{view, Component, Page};23#[derive(Default)]4pub struct Button {5 pub label: String,6 pub disabled: bool,7 pub variant: String,8}910impl Component for Button {11 fn to_render(&self, page: &mut Page) {12 view! {13 <button data-variant={&self.variant}>14 {&self.label}15 </button>16 }17 }18}1920// Only set label, the rest use Default::default()21view! { <Button label={"Save".to_string()} .. /> }2223// All fields use Default24view! { <Button .. /> }
The head! Macro
Call head! to inject elements into the page head. The call is deduplicated by a compile-time UUID, so it is safe to call from components rendered in loops:
1use tidos::{head, view, Component, Page};23impl Component for MyComponent {4 fn to_render(&self, page: &mut Page) {5 head! {6 <link rel="stylesheet" href="/fonts.css" />7 <script src="/analytics.js"></script>8 }9 view! {10 <section>{"Content here"}</section>11 }12 }13}
The scoped_css! Macro
scoped_css! reads a CSS file at compile time, generates a unique class name, and injects a scoped style tag into the head. Apply the returned class to your root element:
1use tidos::{scoped_css, view, Component, Page};23impl Component for Card {4 fn to_render(&self, page: &mut Page) {5 view! {6 <div class={scoped_css!("./card.css")}>7 <h2>{&self.title}</h2>8 </div>9 }10 }11}1213// card.css14// & {15// background: #1a1a2e;16// border-radius: 8px;17// padding: 1.5rem;18// }19// & h2 {20// color: #a8dadc;21// }