Jesús Pérez b6a4d77421
Some checks are pending
Documentation Lint & Validation / Markdown Linting (push) Waiting to run
Documentation Lint & Validation / Validate mdBook Configuration (push) Waiting to run
Documentation Lint & Validation / Content & Structure Validation (push) Waiting to run
Documentation Lint & Validation / Lint & Validation Summary (push) Blocked by required conditions
mdBook Build & Deploy / Build mdBook (push) Waiting to run
mdBook Build & Deploy / Documentation Quality Check (push) Blocked by required conditions
mdBook Build & Deploy / Deploy to GitHub Pages (push) Blocked by required conditions
mdBook Build & Deploy / Notification (push) Blocked by required conditions
Rust CI / Security Audit (push) Waiting to run
Rust CI / Check + Test + Lint (nightly) (push) Waiting to run
Rust CI / Check + Test + Lint (stable) (push) Waiting to run
feat: add Leptos UI library and modularize MCP server
2026-02-14 20:10:55 +00:00

38 KiB

Component Cookbook

Comprehensive examples for all vapora-leptos-ui components.

Table of Contents


Primitives

Button

use leptos::prelude::*;
use vapora_leptos_ui::{Button, Variant, Size};

#[component]
fn ButtonExamples() -> impl IntoView {
    let (count, set_count) = signal(0);

    view! {
        <div class="space-y-4">
            // Primary button
            <Button
                variant=Variant::Primary
                size=Size::Large
                on_click=Some(Callback::new(move |_| {
                    set_count.update(|c| *c += 1);
                }))
            >
                {move || format!("Clicked {} times", count.get())}
            </Button>

            // Secondary button
            <Button variant=Variant::Secondary size=Size::Medium>
                "Cancel"
            </Button>

            // Danger button
            <Button variant=Variant::Danger size=Size::Small>
                "Delete"
            </Button>

            // Ghost button
            <Button variant=Variant::Ghost>
                "Subtle Action"
            </Button>

            // Loading button
            <Button variant=Variant::Primary loading=true>
                "Saving..."
            </Button>

            // Disabled button
            <Button variant=Variant::Primary disabled=true>
                "Disabled"
            </Button>
        </div>
    }
}

Input

use leptos::prelude::*;
use vapora_leptos_ui::Input;

/// Extract value from input event (Leptos 0.8 helper)
#[cfg(target_arch = "wasm32")]
fn event_target_value(ev: &leptos::ev::Event) -> String {
    use wasm_bindgen::JsCast;
    ev.target()
        .and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
        .map(|input| input.value())
        .unwrap_or_default()
}

#[component]
fn InputExamples() -> impl IntoView {
    let (username, set_username) = signal(String::new());
    let (password, set_password) = signal(String::new());
    let (email, set_email) = signal(String::new());

    view! {
        <div class="space-y-4">
            // Text input
            <Input
                input_type="text"
                placeholder="Enter username"
                on_input=Callback::new(move |ev| {
                    set_username.set(event_target_value(&ev));
                })
            />

            // Password input
            <Input
                input_type="password"
                placeholder="Enter password"
                on_input=Callback::new(move |ev| {
                    set_password.set(event_target_value(&ev));
                })
            />

            // Email input
            <Input
                input_type="email"
                placeholder="Enter email"
                on_input=Callback::new(move |ev| {
                    set_email.set(event_target_value(&ev));
                })
            />

            // Disabled input
            <Input
                input_type="text"
                placeholder="Disabled field"
                disabled=true
                on_input=Callback::new(|_| {})
            />

            // Display values
            <div class="text-white">
                <div>"Username: " {move || username.get()}</div>
                <div>"Password: " {move || "*".repeat(password.get().len())}</div>
                <div>"Email: " {move || email.get()}</div>
            </div>
        </div>
    }
}

Badge

use leptos::prelude::*;
use vapora_leptos_ui::Badge;

#[component]
fn BadgeExamples() -> impl IntoView {
    view! {
        <div class="flex gap-2">
            <Badge class="bg-green-500/20 text-green-400">"Active"</Badge>
            <Badge class="bg-blue-500/20 text-blue-400">"Pending"</Badge>
            <Badge class="bg-red-500/20 text-red-400">"Error"</Badge>
            <Badge class="bg-purple-500/20 text-purple-400">"Premium"</Badge>
            <Badge class="bg-gray-500/20 text-gray-400">"Archived"</Badge>
        </div>
    }
}

Spinner

use leptos::prelude::*;
use vapora_leptos_ui::{Spinner, Size};

#[component]
fn SpinnerExamples() -> impl IntoView {
    view! {
        <div class="flex gap-8 items-center">
            <div class="flex flex-col items-center gap-2">
                <Spinner size=Size::Small />
                <span class="text-white text-sm">"Small"</span>
            </div>

            <div class="flex flex-col items-center gap-2">
                <Spinner size=Size::Medium />
                <span class="text-white text-sm">"Medium"</span>
            </div>

            <div class="flex flex-col items-center gap-2">
                <Spinner size=Size::Large />
                <span class="text-white text-sm">"Large"</span>
            </div>
        </div>
    }
}

Layout

Card

use leptos::prelude::*;
use vapora_leptos_ui::{Card, GlowColor, Badge};

#[component]
fn CardExamples() -> impl IntoView {
    view! {
        <div class="grid grid-cols-3 gap-6">
            // Basic card
            <Card>
                <h3 class="text-lg font-semibold text-white mb-2">"Basic Card"</h3>
                <p class="text-gray-400">"Simple glassmorphism card"</p>
            </Card>

            // Hoverable card with glow
            <Card hoverable=true glow=GlowColor::Cyan>
                <h3 class="text-lg font-semibold text-cyan-400 mb-2">"Hoverable"</h3>
                <p class="text-gray-400">"Hover for effect"</p>
            </Card>

            // Card with content
            <Card glow=GlowColor::Purple>
                <div class="flex items-center justify-between mb-2">
                    <h3 class="text-lg font-semibold text-purple-400">"Project"</h3>
                    <Badge class="bg-green-500/20 text-green-400">"Active"</Badge>
                </div>
                <p class="text-gray-400 text-sm mb-3">
                    "Multi-agent development platform"
                </p>
                <div class="flex gap-2">
                    <Badge class="bg-blue-500/20 text-blue-400">"Rust"</Badge>
                    <Badge class="bg-purple-500/20 text-purple-400">"Leptos"</Badge>
                </div>
            </Card>
        </div>
    }
}

Modal

use leptos::prelude::*;
use vapora_leptos_ui::{Modal, Button, Variant};

#[component]
fn ModalExample() -> impl IntoView {
    let (show_modal, set_show_modal) = signal(false);
    let (show_large_modal, set_show_large_modal) = signal(false);

    view! {
        <div class="space-y-4">
            // Trigger buttons
            <Button
                variant=Variant::Primary
                on_click=Some(Callback::new(move |_| set_show_modal.set(true)))
            >
                "Open Modal"
            </Button>

            <Button
                variant=Variant::Secondary
                on_click=Some(Callback::new(move |_| set_show_large_modal.set(true)))
            >
                "Open Large Modal"
            </Button>

            // Simple modal
            <Show when=move || show_modal.get()>
                <Modal on_close=Callback::new(move |_| set_show_modal.set(false))>
                    <h2 class="text-2xl font-bold text-white mb-4">"Confirmation"</h2>
                    <p class="text-gray-300 mb-6">
                        "Are you sure you want to proceed?"
                    </p>
                    <div class="flex gap-3 justify-end">
                        <Button
                            variant=Variant::Secondary
                            on_click=Some(Callback::new(move |_| set_show_modal.set(false)))
                        >
                            "Cancel"
                        </Button>
                        <Button
                            variant=Variant::Primary
                            on_click=Some(Callback::new(move |_| {
                                set_show_modal.set(false);
                                // Perform action
                            }))
                        >
                            "Confirm"
                        </Button>
                    </div>
                </Modal>
            </Show>

            // Large modal with content
            <Show when=move || show_large_modal.get()>
                <Modal on_close=Callback::new(move |_| set_show_large_modal.set(false))>
                    <div class="max-w-2xl">
                        <h2 class="text-2xl font-bold text-white mb-4">"Project Details"</h2>
                        <div class="space-y-4 text-gray-300">
                            <p>"This is a large modal with more content."</p>
                            <p>"Modal features:"</p>
                            <ul class="list-disc list-inside">
                                <li>"Press Escape to close"</li>
                                <li>"Click backdrop to close"</li>
                                <li>"Tab navigation trapped inside"</li>
                                <li>"Auto-focus on first focusable element"</li>
                            </ul>
                        </div>
                        <div class="mt-6 flex justify-end">
                            <Button
                                variant=Variant::Primary
                                on_click=Some(Callback::new(move |_| {
                                    set_show_large_modal.set(false);
                                }))
                            >
                                "Close"
                            </Button>
                        </div>
                    </div>
                </Modal>
            </Show>
        </div>
    }
}

Data

Table

use leptos::prelude::*;
use vapora_leptos_ui::{Table, TableColumn};

#[component]
fn TableExample() -> impl IntoView {
    let columns = vec![
        TableColumn::new("Name", "name").sortable(),
        TableColumn::new("Email", "email").sortable(),
        TableColumn::new("Status", "status").sortable(),
        TableColumn::new("Role", "role"),
    ];

    let rows = vec![
        vec![
            "Alice Smith".to_string(),
            "alice@example.com".to_string(),
            "Active".to_string(),
            "Admin".to_string(),
        ],
        vec![
            "Bob Johnson".to_string(),
            "bob@example.com".to_string(),
            "Inactive".to_string(),
            "User".to_string(),
        ],
        vec![
            "Carol Williams".to_string(),
            "carol@example.com".to_string(),
            "Active".to_string(),
            "Moderator".to_string(),
        ],
    ];

    view! {
        <Table columns=columns rows=rows />
    }
}

Pagination

use leptos::prelude::*;
use vapora_leptos_ui::Pagination;

#[component]
fn PaginationExample() -> impl IntoView {
    let (current_page, set_current_page) = signal(1usize);
    let total_pages = 10;

    view! {
        <div class="space-y-4">
            <div class="text-white text-center">
                "Current page: " {move || current_page.get()}
            </div>

            <Pagination
                current_page=current_page.get()
                total_pages=total_pages
                on_page_change=Callback::new(move |page| {
                    set_current_page.set(page);
                })
            />
        </div>
    }
}

StatCard

use leptos::prelude::*;
use vapora_leptos_ui::StatCard;

#[component]
fn StatCardExamples() -> impl IntoView {
    view! {
        <div class="grid grid-cols-1 md:grid-cols-4 gap-6">
            <StatCard
                label="Total Users".to_string()
                value="1,234".to_string()
            />

            <StatCard
                label="Active Projects".to_string()
                value="42".to_string()
                trend_positive=true
            />

            <StatCard
                label="Revenue".to_string()
                value="$12,345".to_string()
                trend_positive=true
            />

            <StatCard
                label="Pending Tasks".to_string()
                value="8".to_string()
                trend_positive=false
            />
        </div>
    }
}

Forms

FormField

use leptos::prelude::*;
use vapora_leptos_ui::{FormField, Input, Button, Variant};

#[component]
fn FormFieldExample() -> impl IntoView {
    let (username, set_username) = signal(String::new());
    let (email, set_email) = signal(String::new());
    let (username_error, set_username_error) = signal::<Option<String>>(None);

    let handle_submit = move |ev: leptos::ev::SubmitEvent| {
        ev.prevent_default();

        // Validate
        if username.get().trim().is_empty() {
            set_username_error.set(Some("Username is required".to_string()));
            return;
        }

        // Submit form
        // ...
    };

    view! {
        <form on:submit=handle_submit class="space-y-4">
            // Field with error
            {move || {
                if let Some(err) = username_error.get() {
                    view! {
                        <FormField
                            label="Username".to_string()
                            error=err
                            required=true
                            has_error=true
                        >
                            <Input
                                input_type="text"
                                placeholder="Enter username"
                                on_input=Callback::new(move |ev| {
                                    set_username.set(event_target_value(&ev));
                                    set_username_error.set(None);
                                })
                            />
                        </FormField>
                    }.into_any()
                } else {
                    view! {
                        <FormField
                            label="Username".to_string()
                            required=true
                            has_error=false
                        >
                            <Input
                                input_type="text"
                                placeholder="Enter username"
                                on_input=Callback::new(move |ev| {
                                    set_username.set(event_target_value(&ev));
                                })
                            />
                        </FormField>
                    }.into_any()
                }
            }}

            // Field with help text
            <FormField
                label="Email".to_string()
                help_text="We'll never share your email".to_string()
                required=false
                has_error=false
            >
                <Input
                    input_type="email"
                    placeholder="you@example.com"
                    on_input=Callback::new(move |ev| {
                        set_email.set(event_target_value(&ev));
                    })
                />
            </FormField>

            <Button variant=Variant::Primary button_type="submit">
                "Submit"
            </Button>
        </form>
    }
}

Validation

use leptos::prelude::*;
use vapora_leptos_ui::{
    validate_required, validate_email, validate_min_length, validate_max_length,
    FormField, Input, Button, Variant
};

#[component]
fn ValidationExample() -> impl IntoView {
    let (email, set_email) = signal(String::new());
    let (password, set_password) = signal(String::new());
    let (email_error, set_email_error) = signal::<Option<String>>(None);
    let (password_error, set_password_error) = signal::<Option<String>>(None);

    let validate_form = move || -> bool {
        let mut valid = true;

        // Validate email
        if let Err(err) = validate_required(&email.get(), "Email") {
            set_email_error.set(Some(err));
            valid = false;
        } else if let Err(err) = validate_email(&email.get()) {
            set_email_error.set(Some(err));
            valid = false;
        } else {
            set_email_error.set(None);
        }

        // Validate password
        if let Err(err) = validate_required(&password.get(), "Password") {
            set_password_error.set(Some(err));
            valid = false;
        } else if let Err(err) = validate_min_length(&password.get(), 8, "Password") {
            set_password_error.set(Some(err));
            valid = false;
        } else if let Err(err) = validate_max_length(&password.get(), 100, "Password") {
            set_password_error.set(Some(err));
            valid = false;
        } else {
            set_password_error.set(None);
        }

        valid
    };

    view! {
        <form on:submit=move |ev| {
            ev.prevent_default();
            if validate_form() {
                // Submit
            }
        } class="space-y-4">
            // Email field with validation
            {move || {
                if let Some(err) = email_error.get() {
                    view! {
                        <FormField label="Email".to_string() error=err required=true has_error=true>
                            <Input
                                input_type="email"
                                placeholder="you@example.com"
                                on_input=Callback::new(move |ev| {
                                    set_email.set(event_target_value(&ev));
                                    set_email_error.set(None);
                                })
                            />
                        </FormField>
                    }.into_any()
                } else {
                    view! {
                        <FormField label="Email".to_string() required=true has_error=false>
                            <Input
                                input_type="email"
                                placeholder="you@example.com"
                                on_input=Callback::new(move |ev| {
                                    set_email.set(event_target_value(&ev));
                                })
                            />
                        </FormField>
                    }.into_any()
                }
            }}

            // Password field with validation
            {move || {
                if let Some(err) = password_error.get() {
                    view! {
                        <FormField
                            label="Password".to_string()
                            error=err
                            required=true
                            has_error=true
                            help_text="8-100 characters".to_string()
                        >
                            <Input
                                input_type="password"
                                placeholder="Enter password"
                                on_input=Callback::new(move |ev| {
                                    set_password.set(event_target_value(&ev));
                                    set_password_error.set(None);
                                })
                            />
                        </FormField>
                    }.into_any()
                } else {
                    view! {
                        <FormField
                            label="Password".to_string()
                            required=true
                            has_error=false
                            help_text="8-100 characters".to_string()
                        >
                            <Input
                                input_type="password"
                                placeholder="Enter password"
                                on_input=Callback::new(move |ev| {
                                    set_password.set(event_target_value(&ev));
                                })
                            />
                        </FormField>
                    }.into_any()
                }
            }}

            <Button variant=Variant::Primary button_type="submit">
                "Sign Up"
            </Button>
        </form>
    }
}

Feedback

Toast

use leptos::prelude::*;
use vapora_leptos_ui::{ToastProvider, use_toast, ToastType, Button, Variant};

#[component]
fn ToastExample() -> impl IntoView {
    view! {
        <ToastProvider>
            <ToastButtons />
        </ToastProvider>
    }
}

#[component]
fn ToastButtons() -> impl IntoView {
    let toast = use_toast();

    view! {
        <div class="space-x-4">
            <Button
                variant=Variant::Primary
                on_click=Some(Callback::new(move |_| {
                    toast.show_toast(
                        "Operation successful!".to_string(),
                        ToastType::Success
                    );
                }))
            >
                "Show Success"
            </Button>

            <Button
                variant=Variant::Danger
                on_click=Some(Callback::new(move |_| {
                    toast.show_toast(
                        "Something went wrong!".to_string(),
                        ToastType::Error
                    );
                }))
            >
                "Show Error"
            </Button>

            <Button
                variant=Variant::Secondary
                on_click=Some(Callback::new(move |_| {
                    toast.show_toast(
                        "Here's some information".to_string(),
                        ToastType::Info
                    );
                }))
            >
                "Show Info"
            </Button>
        </div>
    }
}

Navigation

use leptos::prelude::*;
use vapora_leptos_ui::SpaLink;

#[component]
fn SpaLinkExamples() -> impl IntoView {
    view! {
        <div class="space-y-4">
            // Internal link (SPA navigation)
            <SpaLink
                href="/projects".to_string()
                class=Some("text-cyan-400 hover:text-cyan-300".to_string())
            >
                "View Projects"
            </SpaLink>

            // External link (opens in new tab)
            <SpaLink
                href="https://github.com".to_string()
                external=true
                class=Some("text-purple-400 hover:text-purple-300".to_string())
            >
                "GitHub"
            </SpaLink>

            // Mailto link (external)
            <SpaLink
                href="mailto:support@example.com".to_string()
                class=Some("text-blue-400 hover:text-blue-300".to_string())
            >
                "Email Support"
            </SpaLink>
        </div>
    }
}

Patterns

Modal with Form

Complete example of a modal containing a validated form:

use leptos::prelude::*;
use vapora_leptos_ui::{
    Modal, FormField, Input, Button, Variant, use_toast, ToastType,
    validate_required
};

#[component]
fn CreateProjectModal() -> impl IntoView {
    let (show_modal, set_show_modal) = signal(false);
    let (title, set_title) = signal(String::new());
    let (description, set_description) = signal(String::new());
    let (error, set_error) = signal::<Option<String>>(None);
    let toast = use_toast();

    let handle_submit = move |ev: leptos::ev::SubmitEvent| {
        ev.prevent_default();

        // Validate
        if let Err(err) = validate_required(&title.get(), "Project title") {
            set_error.set(Some(err));
            return;
        }

        // Submit (in real app, call API)
        toast.show_toast(
            format!("Created project: {}", title.get()),
            ToastType::Success
        );

        // Reset and close
        set_title.set(String::new());
        set_description.set(String::new());
        set_error.set(None);
        set_show_modal.set(false);
    };

    view! {
        <div>
            <Button
                variant=Variant::Primary
                on_click=Some(Callback::new(move |_| set_show_modal.set(true)))
            >
                "+ New Project"
            </Button>

            <Show when=move || show_modal.get()>
                <Modal on_close=Callback::new(move |_| {
                    set_show_modal.set(false);
                    set_title.set(String::new());
                    set_description.set(String::new());
                    set_error.set(None);
                })>
                    <h2 class="text-2xl font-bold text-white mb-6">"Create New Project"</h2>

                    <form on:submit=handle_submit>
                        <div class="flex flex-col gap-4">
                            // Title field with validation
                            {move || {
                                if let Some(err) = error.get() {
                                    view! {
                                        <FormField
                                            label="Project Title".to_string()
                                            error=err
                                            required=true
                                            has_error=true
                                        >
                                            <Input
                                                input_type="text"
                                                placeholder="Enter project name"
                                                on_input=Callback::new(move |ev| {
                                                    set_title.set(event_target_value(&ev));
                                                    set_error.set(None);
                                                })
                                            />
                                        </FormField>
                                    }.into_any()
                                } else {
                                    view! {
                                        <FormField
                                            label="Project Title".to_string()
                                            required=true
                                            has_error=false
                                        >
                                            <Input
                                                input_type="text"
                                                placeholder="Enter project name"
                                                on_input=Callback::new(move |ev| {
                                                    set_title.set(event_target_value(&ev));
                                                })
                                            />
                                        </FormField>
                                    }.into_any()
                                }
                            }}

                            // Description field (optional)
                            <FormField
                                label="Description".to_string()
                                help_text="Optional project description".to_string()
                                required=false
                                has_error=false
                            >
                                <Input
                                    input_type="text"
                                    placeholder="Describe your project"
                                    on_input=Callback::new(move |ev| {
                                        set_description.set(event_target_value(&ev));
                                    })
                                />
                            </FormField>

                            // Action buttons
                            <div class="flex gap-3 justify-end mt-4">
                                <Button
                                    variant=Variant::Secondary
                                    on_click=Some(Callback::new(move |_| {
                                        set_show_modal.set(false);
                                    }))
                                >
                                    "Cancel"
                                </Button>
                                <Button variant=Variant::Primary button_type="submit">
                                    "Create Project"
                                </Button>
                            </div>
                        </div>
                    </form>
                </Modal>
            </Show>
        </div>
    }
}

Table with Sorting and Pagination

Complete example of a data table with sorting and pagination:

use leptos::prelude::*;
use vapora_leptos_ui::{Table, TableColumn, Pagination, Spinner};

const ITEMS_PER_PAGE: usize = 10;

#[derive(Clone, Debug)]
struct User {
    id: String,
    name: String,
    email: String,
    status: String,
    role: String,
}

#[component]
fn UsersTable() -> impl IntoView {
    let (users, set_users) = signal(Vec::<User>::new());
    let (loading, set_loading) = signal(true);
    let (current_page, set_current_page) = signal(1usize);

    // Fetch users (simulated)
    Effect::new(move |_| {
        spawn_local(async move {
            // Simulate API call
            tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;

            let mock_users = vec![
                User {
                    id: "1".to_string(),
                    name: "Alice Smith".to_string(),
                    email: "alice@example.com".to_string(),
                    status: "Active".to_string(),
                    role: "Admin".to_string(),
                },
                // ... more users
            ];

            set_users.set(mock_users);
            set_loading.set(false);
        });
    });

    view! {
        <div class="space-y-6">
            <Show
                when=move || !loading.get()
                fallback=|| view! {
                    <div class="flex flex-col items-center justify-center py-12 gap-4">
                        <Spinner />
                        <div class="text-lg text-white/80">"Loading users..."</div>
                    </div>
                }
            >
                {move || {
                    let all_users = users.get();
                    let total_items = all_users.len();
                    let total_pages = total_items.div_ceil(ITEMS_PER_PAGE);

                    // Paginate
                    let page = current_page.get();
                    let start_idx = (page - 1) * ITEMS_PER_PAGE;
                    let end_idx = (start_idx + ITEMS_PER_PAGE).min(total_items);

                    // Convert to table rows
                    let columns = vec![
                        TableColumn::new("Name", "name").sortable(),
                        TableColumn::new("Email", "email").sortable(),
                        TableColumn::new("Status", "status").sortable(),
                        TableColumn::new("Role", "role"),
                    ];

                    let rows: Vec<Vec<String>> = all_users
                        .into_iter()
                        .skip(start_idx)
                        .take(end_idx - start_idx)
                        .map(|user| {
                            vec![user.name, user.email, user.status, user.role]
                        })
                        .collect();

                    view! {
                        <div class="flex flex-col gap-6">
                            <Table columns=columns rows=rows />

                            {move || {
                                if total_pages > 1 {
                                    view! {
                                        <div class="flex justify-center">
                                            <Pagination
                                                current_page=current_page.get()
                                                total_pages=total_pages
                                                on_page_change=Callback::new(move |page| {
                                                    set_current_page.set(page);
                                                })
                                            />
                                        </div>
                                    }.into_any()
                                } else {
                                    view! { <div /> }.into_any()
                                }
                            }}
                        </div>
                    }
                }}
            </Show>
        </div>
    }
}

Dashboard with Stats

Complete dashboard example with stat cards and data display:

use leptos::prelude::*;
use vapora_leptos_ui::{StatCard, Card, Badge, GlowColor, Spinner};

#[component]
fn Dashboard() -> impl IntoView {
    let (loading, set_loading) = signal(true);
    let (total_users, set_total_users) = signal(0);
    let (active_projects, set_active_projects) = signal(0);
    let (revenue, set_revenue) = signal(0);

    // Fetch stats
    Effect::new(move |_| {
        spawn_local(async move {
            // Simulate API call
            tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;

            set_total_users.set(1234);
            set_active_projects.set(42);
            set_revenue.set(12345);
            set_loading.set(false);
        });
    });

    view! {
        <div class="space-y-8">
            <h1 class="text-3xl font-bold text-white">"Dashboard"</h1>

            <Show
                when=move || !loading.get()
                fallback=|| view! {
                    <div class="flex justify-center py-12">
                        <Spinner />
                    </div>
                }
            >
                // Stats cards
                <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
                    <StatCard
                        label="Total Users".to_string()
                        value=move || total_users.get().to_string()
                    />
                    <StatCard
                        label="Active Projects".to_string()
                        value=move || active_projects.get().to_string()
                        trend_positive=true
                    />
                    <StatCard
                        label="Revenue".to_string()
                        value=move || format!("${}", revenue.get())
                        trend_positive=true
                    />
                </div>

                // Recent projects
                <div>
                    <h2 class="text-2xl font-semibold text-white mb-4">"Recent Projects"</h2>
                    <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
                        <Card glow=GlowColor::Cyan hoverable=true>
                            <div class="flex items-center justify-between mb-2">
                                <h3 class="text-lg font-semibold text-cyan-400">"Project Alpha"</h3>
                                <Badge class="bg-green-500/20 text-green-400">"Active"</Badge>
                            </div>
                            <p class="text-gray-400 text-sm">
                                "Multi-agent development platform"
                            </p>
                        </Card>

                        <Card glow=GlowColor::Purple hoverable=true>
                            <div class="flex items-center justify-between mb-2">
                                <h3 class="text-lg font-semibold text-purple-400">"Project Beta"</h3>
                                <Badge class="bg-blue-500/20 text-blue-400">"In Progress"</Badge>
                            </div>
                            <p class="text-gray-400 text-sm">
                                "AI-powered code reviews"
                            </p>
                        </Card>

                        <Card glow=GlowColor::Pink hoverable=true>
                            <div class="flex items-center justify-between mb-2">
                                <h3 class="text-lg font-semibold text-pink-400">"Project Gamma"</h3>
                                <Badge class="bg-gray-500/20 text-gray-400">"Completed"</Badge>
                            </div>
                            <p class="text-gray-400 text-sm">
                                "Automated testing suite"
                            </p>
                        </Card>
                    </div>
                </div>
            </Show>
        </div>
    }
}

Best Practices

Event Handling

Always use Callback::new() for event handlers:

// ✅ CORRECT
<Button on_click=Some(Callback::new(move |_| { /* handler */ }))>

// ❌ WRONG (type mismatch)
<Button on_click=Some(move |_| { /* handler */ })>

Optional Props

For props marked #[prop(optional)] with Option<T>:

// ✅ CORRECT - Pass T directly (gets wrapped in Some)
<FormField error="Username is required".to_string() />

// ✅ CORRECT - Omit prop entirely (becomes None)
<FormField />

// ❌ WRONG - Don't pass Option<T>
<FormField error=Some("Error".to_string()) />  // Type mismatch

Conditional Rendering

Use conditional rendering for optional props with dynamic values:

{move || {
    if let Some(err) = error.get() {
        view! { <FormField error=err /> }
    } else {
        view! { <FormField /> }
    }
}}

Extract Input Values

Helper for Leptos 0.8 (no built-in event_target_value):

#[cfg(target_arch = "wasm32")]
fn event_target_value(ev: &leptos::ev::Event) -> String {
    use wasm_bindgen::JsCast;
    ev.target()
        .and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
        .map(|input| input.value())
        .unwrap_or_default()
}

Last Updated: 2026-02-08 (v1.2.0)