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

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// 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<'a> {
12 pub title: String,
13 pub body: Slot<'a>,
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}

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} wrapper
2view! {
3 <Card>
4 <p>{"This content goes straight into the card."}</p>
5 </Card>
6}
7
8// Child component: a tuple struct with Slot<'a> as field 0
9pub struct Card<'a>(pub Slot<'a>);
10
11impl 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};
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// }