スキル一覧に戻る
matthewharwood

maud-components-patterns

by matthewharwood

A personal engineering-management site exploring systems leadership, design-driven engineering, and building teams that ship. Notes on architecture, platforms, culture, and the craft of leading engineers at scale.

2🍴 0📅 2026年1月3日
GitHubで見るManusで実行

SKILL.md


name: maud-components-patterns description: Reusable component patterns for Maud including the Render trait, function components, parameterized components, layout composition, partials, and component organization. Use when building reusable UI elements, creating component libraries, structuring templates, or implementing design systems with type-safe components.

Maud Component Patterns

Production patterns for building reusable, type-safe HTML components with Maud

Version Context

  • Maud: 0.27.0
  • Rust Edition: 2021

When to Use This Skill

  • Creating reusable UI components
  • Building component libraries
  • Implementing design systems in Rust
  • Structuring template code for maintainability
  • Composing complex layouts from smaller pieces
  • Building type-safe HTML abstractions

The Render Trait

The Render trait is Maud's primary mechanism for type-safe component composition.

Basic Render Implementation

use maud::{html, Markup, Render};

struct User {
    name: String,
    email: String,
    role: UserRole,
}

enum UserRole {
    Admin,
    User,
    Guest,
}

impl Render for User {
    fn render(&self) -> Markup {
        html! {
            div.user-card {
                h3.user-name { (self.name) }
                p.user-email { (self.email) }
                span.user-role {
                    @match self.role {
                        UserRole::Admin => span.badge.admin { "Admin" }
                        UserRole::User => span.badge.user { "User" }
                        UserRole::Guest => span.badge.guest { "Guest" }
                    }
                }
            }
        }
    }
}

// Usage: automatically calls render()
let user = User {
    name: "Alice".to_string(),
    email: "alice@example.com".to_string(),
    role: UserRole::Admin,
};

html! {
    div.user-list {
        (user)  // Renders the user card
    }
}

Render Trait for Domain Types

use uuid::Uuid;

#[derive(Clone, Copy, Debug)]
struct UserId(Uuid);

impl UserId {
    fn new() -> Self {
        Self(Uuid::new_v4())
    }
}

impl Render for UserId {
    fn render(&self) -> Markup {
        html! {
            span.user-id data-id=(self.0.to_string()) {
                (self.0.to_string())
            }
        }
    }
}

// Usage in templates
html! {
    div.user-info {
        "User ID: " (user_id)
    }
}

Function Components

Function components are pure functions that return Markup. They're the most common pattern for reusable components.

Basic Function Component

fn button(text: &str, variant: &str) -> Markup {
    html! {
        button class=(format!("btn btn-{}", variant)) {
            (text)
        }
    }
}

// Usage
html! {
    div.actions {
        (button("Save", "primary"))
        (button("Cancel", "secondary"))
    }
}

Parameterized Components

fn card(
    title: &str,
    description: &str,
    image_url: Option<&str>,
    highlighted: bool,
) -> Markup {
    html! {
        div.card[highlighted] {
            @if let Some(url) = image_url {
                img.card-image src=(url) alt=(title);
            }
            div.card-content {
                h3.card-title { (title) }
                p.card-description { (description) }
            }
        }
    }
}

// Usage
html! {
    div.grid {
        (card(
            "Product 1",
            "Description here",
            Some("/images/product1.jpg"),
            true
        ))
        (card(
            "Product 2",
            "Another description",
            None,
            false
        ))
    }
}

Components with Closures (Content Slots)

fn modal(title: &str, content: Markup) -> Markup {
    html! {
        div.modal {
            div.modal-overlay {}
            div.modal-content {
                header.modal-header {
                    h2 { (title) }
                    button.close-btn aria-label="Close" { "×" }
                }
                div.modal-body {
                    (content)
                }
            }
        }
    }
}

// Usage with nested content
html! {
    (modal("Confirm Action", html! {
        p { "Are you sure you want to proceed?" }
        div.modal-actions {
            button.btn-primary { "Confirm" }
            button.btn-secondary { "Cancel" }
        }
    }))
}

Layout Patterns

Base Layout

use maud::{DOCTYPE, html, Markup};

fn base_layout(
    title: &str,
    description: Option<&str>,
    content: Markup,
) -> Markup {
    html! {
        (DOCTYPE)
        html lang="en" {
            head {
                meta charset="UTF-8";
                meta name="viewport" content="width=device-width, initial-scale=1.0";
                title { (title) }

                @if let Some(desc) = description {
                    meta name="description" content=(desc);
                }

                link rel="stylesheet" href="/static/styles.css";
                script src="https://unpkg.com/htmx.org@2.0.0" {}
            }
            body {
                (content)
            }
        }
    }
}

Layout with Header/Footer

fn page_layout(
    title: &str,
    current_page: &str,
    content: Markup,
) -> Markup {
    base_layout(
        title,
        None,
        html! {
            (header(current_page))
            main.container {
                (content)
            }
            (footer())
        }
    )
}

fn header(current_page: &str) -> Markup {
    html! {
        header.site-header {
            nav.navbar {
                a.logo href="/" { "MyApp" }
                div.nav-links {
                    (nav_link("/", "Home", current_page))
                    (nav_link("/about", "About", current_page))
                    (nav_link("/blog", "Blog", current_page))
                    (nav_link("/contact", "Contact", current_page))
                }
            }
        }
    }
}

fn footer() -> Markup {
    html! {
        footer.site-footer {
            p { "© 2025 MyApp. All rights reserved." }
            div.footer-links {
                a href="/privacy" { "Privacy" }
                a href="/terms" { "Terms" }
            }
        }
    }
}

Authenticated Layout

fn authenticated_layout(
    user: &User,
    page_title: &str,
    content: Markup,
) -> Markup {
    base_layout(
        page_title,
        None,
        html! {
            header.authenticated-header {
                nav {
                    a.logo href="/dashboard" { "Dashboard" }
                    div.user-menu {
                        span.user-name { (user.name) }
                        a href="/profile" { "Profile" }
                        a href="/settings" { "Settings" }
                        form method="POST" action="/logout" {
                            button.btn-link { "Logout" }
                        }
                    }
                }
            }
            main {
                (content)
            }
        }
    )
}

Common UI Components

fn nav_link(href: &str, text: &str, current_path: &str) -> Markup {
    let is_active = current_path == href || current_path.starts_with(href);

    html! {
        a.nav-link[is_active] href=(href) {
            (text)
        }
    }
}

Form Field with Error

fn text_field(
    name: &str,
    label: &str,
    value: Option<&str>,
    error: Option<&str>,
    required: bool,
) -> Markup {
    html! {
        div.form-group[error.is_some()] {
            label for=(name) {
                (label)
                @if required {
                    span.required { "*" }
                }
            }
            input
                type="text"
                name=(name)
                id=(name)
                value=[value]
                required[required];

            @if let Some(err) = error {
                span.error-message { (err) }
            }
        }
    }
}

// Usage
html! {
    form method="POST" {
        (text_field("email", "Email Address", None, None, true))
        (text_field("name", "Full Name", Some("John"), Some("Name is required"), true))
    }
}

Select Dropdown

fn select_field<T: AsRef<str>>(
    name: &str,
    label: &str,
    options: &[(T, T)],  // (value, display_text)
    selected: Option<&str>,
) -> Markup {
    html! {
        div.form-group {
            label for=(name) { (label) }
            select name=(name) id=(name) {
                @for (value, text) in options {
                    option
                        value=(value.as_ref())
                        selected[selected == Some(value.as_ref())]
                    {
                        (text.as_ref())
                    }
                }
            }
        }
    }
}

// Usage
let roles = vec![
    ("admin", "Administrator"),
    ("user", "Regular User"),
    ("guest", "Guest"),
];

html! {
    (select_field("role", "User Role", &roles, Some("user")))
}

Alert/Message Box

enum AlertVariant {
    Info,
    Success,
    Warning,
    Error,
}

impl AlertVariant {
    fn class(&self) -> &'static str {
        match self {
            AlertVariant::Info => "alert-info",
            AlertVariant::Success => "alert-success",
            AlertVariant::Warning => "alert-warning",
            AlertVariant::Error => "alert-error",
        }
    }

    fn icon(&self) -> &'static str {
        match self {
            AlertVariant::Info => "ℹ",
            AlertVariant::Success => "✓",
            AlertVariant::Warning => "⚠",
            AlertVariant::Error => "✕",
        }
    }
}

fn alert(variant: AlertVariant, message: &str, dismissible: bool) -> Markup {
    html! {
        div.alert class=(variant.class()) role="alert" {
            span.alert-icon { (variant.icon()) }
            span.alert-message { (message) }
            @if dismissible {
                button.alert-close aria-label="Close" { "×" }
            }
        }
    }
}

// Usage
html! {
    (alert(AlertVariant::Success, "User created successfully!", true))
    (alert(AlertVariant::Error, "Failed to save changes", false))
}

Card Component

fn card_with_actions(
    title: &str,
    content: Markup,
    actions: Vec<(&str, &str)>,  // (text, href)
) -> Markup {
    html! {
        div.card {
            div.card-header {
                h3 { (title) }
            }
            div.card-body {
                (content)
            }
            div.card-footer {
                @for (text, href) in actions {
                    a.card-action href=(href) { (text) }
                }
            }
        }
    }
}

// Usage
html! {
    (card_with_actions(
        "User Profile",
        html! {
            p { "Name: Alice Johnson" }
            p { "Email: alice@example.com" }
        },
        vec![
            ("Edit", "/users/1/edit"),
            ("Delete", "/users/1/delete"),
        ]
    ))
}

Table Component

fn table<T>(
    headers: &[&str],
    rows: &[T],
    render_row: impl Fn(&T) -> Markup,
) -> Markup {
    html! {
        table.data-table {
            thead {
                tr {
                    @for header in headers {
                        th { (header) }
                    }
                }
            }
            tbody {
                @for row in rows {
                    (render_row(row))
                }
            }
        }
    }
}

// Usage
struct Product {
    id: u64,
    name: String,
    price: f64,
}

let products = vec![
    Product { id: 1, name: "Widget".to_string(), price: 19.99 },
    Product { id: 2, name: "Gadget".to_string(), price: 29.99 },
];

html! {
    (table(
        &["ID", "Name", "Price", "Actions"],
        &products,
        |product| html! {
            tr {
                td { (product.id) }
                td { (product.name) }
                td { "$" (product.price) }
                td {
                    a href={"/products/" (product.id)} { "View" }
                }
            }
        }
    ))
}

Pagination Component

fn pagination(current_page: u32, total_pages: u32, base_url: &str) -> Markup {
    html! {
        nav.pagination aria-label="Pagination" {
            @if current_page > 1 {
                a.page-link href={(base_url) "?page=" (current_page - 1)} {
                    "← Previous"
                }
            } @else {
                span.page-link.disabled { "← Previous" }
            }

            span.page-info {
                "Page " (current_page) " of " (total_pages)
            }

            @if current_page < total_pages {
                a.page-link href={(base_url) "?page=" (current_page + 1)} {
                    "Next →"
                }
            } @else {
                span.page-link.disabled { "Next →" }
            }
        }
    }
}
struct Breadcrumb {
    label: String,
    href: Option<String>,
}

fn breadcrumbs(items: &[Breadcrumb]) -> Markup {
    html! {
        nav.breadcrumbs aria-label="Breadcrumb" {
            ol.breadcrumb-list {
                @for (i, item) in items.iter().enumerate() {
                    li.breadcrumb-item[i == items.len() - 1] {
                        @if let Some(href) = &item.href {
                            a href=(href) { (item.label) }
                        } @else {
                            span { (item.label) }
                        }

                        @if i < items.len() - 1 {
                            span.breadcrumb-separator { "/" }
                        }
                    }
                }
            }
        }
    }
}

// Usage
let crumbs = vec![
    Breadcrumb { label: "Home".to_string(), href: Some("/".to_string()) },
    Breadcrumb { label: "Products".to_string(), href: Some("/products".to_string()) },
    Breadcrumb { label: "Widget".to_string(), href: None },
];

html! {
    (breadcrumbs(&crumbs))
}

Component Organization

File Structure

src/
├── main.rs
├── routes/
│   ├── mod.rs
│   ├── home.rs
│   ├── users.rs
│   └── products.rs
├── templates/
│   ├── mod.rs
│   ├── layouts/
│   │   ├── mod.rs
│   │   ├── base.rs
│   │   ├── authenticated.rs
│   │   └── guest.rs
│   ├── components/
│   │   ├── mod.rs
│   │   ├── forms.rs
│   │   ├── navigation.rs
│   │   ├── cards.rs
│   │   └── tables.rs
│   └── pages/
│       ├── mod.rs
│       ├── home.rs
│       ├── about.rs
│       └── error.rs

Module Organization (templates/mod.rs)

pub mod layouts;
pub mod components;
pub mod pages;

// Re-export commonly used items
pub use layouts::{base_layout, authenticated_layout};
pub use components::forms::{text_field, select_field};
pub use components::navigation::{nav_link, breadcrumbs};

Component Module (templates/components/forms.rs)

use maud::{html, Markup};

pub fn text_field(
    name: &str,
    label: &str,
    value: Option<&str>,
    error: Option<&str>,
) -> Markup {
    html! {
        div.form-group {
            label for=(name) { (label) }
            input type="text" name=(name) id=(name) value=[value];
            @if let Some(err) = error {
                span.error { (err) }
            }
        }
    }
}

pub fn submit_button(text: &str, disabled: bool) -> Markup {
    html! {
        button.btn.btn-primary type="submit" disabled[disabled] {
            (text)
        }
    }
}

Advanced Patterns

Component Builder Pattern

pub struct CardBuilder {
    title: String,
    content: Markup,
    footer: Option<Markup>,
    variant: Option<String>,
}

impl CardBuilder {
    pub fn new(title: impl Into<String>) -> Self {
        Self {
            title: title.into(),
            content: html! {},
            footer: None,
            variant: None,
        }
    }

    pub fn content(mut self, content: Markup) -> Self {
        self.content = content;
        self
    }

    pub fn footer(mut self, footer: Markup) -> Self {
        self.footer = Some(footer);
        self
    }

    pub fn variant(mut self, variant: impl Into<String>) -> Self {
        self.variant = Some(variant.into());
        self
    }

    pub fn build(self) -> Markup {
        html! {
            div.card class=[self.variant] {
                div.card-header {
                    h3 { (self.title) }
                }
                div.card-body {
                    (self.content)
                }
                @if let Some(footer) = self.footer {
                    div.card-footer {
                        (footer)
                    }
                }
            }
        }
    }
}

// Usage
html! {
    (CardBuilder::new("User Profile")
        .content(html! { p { "User details here" } })
        .footer(html! { button { "Edit" } })
        .variant("highlighted")
        .build())
}

Generic Component with Type Parameters

fn list<T>(items: &[T], render_item: impl Fn(&T) -> Markup) -> Markup {
    html! {
        ul.item-list {
            @for item in items {
                li.list-item {
                    (render_item(item))
                }
            }
        }
    }
}

// Usage
let users = vec!["Alice", "Bob", "Charlie"];

html! {
    (list(&users, |name| html! {
        span.user-name { (name) }
    }))
}

Conditional Component Rendering

fn render_if(condition: bool, component: impl FnOnce() -> Markup) -> Markup {
    if condition {
        component()
    } else {
        html! {}
    }
}

// Usage
html! {
    div {
        (render_if(user.is_admin(), || {
            html! {
                button.admin-panel { "Admin Panel" }
            }
        }))
    }
}

Best Practices

  1. Keep components pure: Components should be functions without side effects
  2. Use descriptive names: user_profile_card not card1
  3. Parameterize behavior: Use function parameters for variations, not duplication
  4. Compose from small pieces: Build complex components from simpler ones
  5. Type safety: Use enums for variants instead of strings when possible
  6. Organize by domain: Group related components together
  7. Document parameters: Add doc comments for complex component signatures

Component Design Guidelines

Good Component Design

// ✅ Good: Type-safe, explicit parameters
fn button(text: &str, variant: ButtonVariant, disabled: bool) -> Markup {
    html! {
        button.btn class=(variant.class()) disabled[disabled] {
            (text)
        }
    }
}

enum ButtonVariant {
    Primary,
    Secondary,
    Danger,
}

impl ButtonVariant {
    fn class(&self) -> &'static str {
        match self {
            ButtonVariant::Primary => "btn-primary",
            ButtonVariant::Secondary => "btn-secondary",
            ButtonVariant::Danger => "btn-danger",
        }
    }
}

Poor Component Design

// ❌ Bad: Stringly-typed, unclear parameters
fn button(text: &str, class: &str, attrs: &str) -> Markup {
    html! {
        button class=(class) (PreEscaped(attrs)) {
            (text)
        }
    }
}

References

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です