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;
4
5view! {
6 // --- Text content ---
7
8 // Static text
9 <p>{"Hello world"}</p>
10
11 // Formatted, with {} placeholders
12 <p>{"Hello {}, you have {} items.", name, count}</p>
13
14 // Any Rust expression
15 <p>{count.to_string()}</p>
16
17 // Raw HTML (not sanitized) — use with care!
18 <div>@html{"<em>italic</em>"}</div>
19
20 // --- Attributes share the exact same content syntax ---
21
22 // Static value
23 <span class={"rounded"}></span>
24
25 // Formatted value (here combined with scoped_css!)
26 <span class={"rounded {}", scoped_css!("./mod.css")}></span>
27
28 // Any Rust expression
29 <div data-count={count.to_string()}></div>
30
31 // 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};
2
3pub struct Alert {
4 pub message: String,
5 pub kind: String,
6}
7
8impl 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;
3
4view! {
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"];
2
3view! {
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;
3
4view! {
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

Named slots let a parent pass rendered HTML content into a child component. Use {#slot:name}...{/slot} in the parent and @slot{self.name} in the child:

1// Parent
2view! {
3 <Card title={"News".to_string()}>
4 {#slot:body}
5 <p>{"This content goes into the card body."}</p>
6 {/slot}
7 </Card>
8}
9
10// Child component
11pub struct Card {
12 pub title: String,
13 pub body: tidos::Slot,
14}
15
16impl 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}

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};
2
3#[derive(Default)]
4pub struct Button {
5 pub label: String,
6 pub disabled: bool,
7 pub variant: String,
8}
9
10impl 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}
19
20// Only set label — the rest use Default::default()
21view! { <Button label={"Save".to_string()} .. /> }
22
23// All fields use Default
24view! { <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};
2
3impl 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};
2
3impl 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}
12
13// card.css
14// & {
15// background: #1a1a2e;
16// border-radius: 8px;
17// padding: 1.5rem;
18// }
19// & h2 {
20// color: #a8dadc;
21// }