Component
A component is any Rust struct that implements the Component trait. Components are the building blocks of Tidos, they encapsulate HTML structure, styles, and logic.
The view! Macro
view! is the templating macro. Use it inside the to_render function of the Component trait to write HTML with Rust expressions. All text between tags must be in {}. Attribute values use that same {} syntax — literals, formatted strings, or any expression — and a : prefix turns an attribute into a toggle that is either present or absent (like disabled), rather than 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 on any struct. The to_render method receives a mutable Page reference so child components can inject CSS or head elements:
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 the content and renders it with @slot{self.field}. There are two forms — pick based 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// }