Small, local superpowers for Maud.
Recommended: install the crate as mx so the macro and component surface reads
well at call sites:
cargo add maud-extensions --rename mxIf you want the experimental component system too, enable components on the
same dependency:
[dependencies]
mx = { package = "maud-extensions", version = "0.6.7", features = ["components"] }Write plain html! and emit local CSS and JS where they belong:
use maud::html;
use maud_extensions::{css, js};
fn status_card(message: &str) -> maud::Markup {
html! {
article class="status-card" {
h2 { "System status" }
p class="message" { (message) }
(css! {
me {
border: 1px solid #ddd;
border-radius: 10px;
padding: 12px;
}
me.ready {
border-color: #16a34a;
}
})
(js!(once, {
me().class_add("ready");
}))
}
}
}This is still the intended center of gravity:
- no wrapper component macro
- no hidden CSS/JS injection
- no stringly helper names
- plain Maud remains the main language
The component system is opt-in behind the components feature.
Preferred authoring pattern:
use maud::Markup;
use mx::{Component, Slot};
#[derive(Component)]
struct Card {
title: String,
header: Slot<Markup>,
#[mx(default)]
body: Slot<Markup>,
footer: Slot<Markup>,
#[mx(each = action)]
actions: Slot<Vec<Markup>>,
}
impl Card {
fn css() -> Markup {
mx::css! {
me {
padding: 1rem;
border: 1px solid #ddd;
}
me .actions {
display: flex;
gap: 0.5rem;
}
}
}
fn js() -> Markup {
mx::js!(once, {
me().class_add("ready");
})
}
}
impl maud::Render for Card {
fn render(&self) -> Markup {
maud::html! {
article.card {
(Self::css())
(Self::js())
header class="header" { (self.header) }
h2 { (self.title) }
div.body { (self.body) }
footer class="footer" { (self.footer) }
div.actions { (self.actions) }
}
}
}
}
fn view() -> Markup {
Card::new()
.title("Profile")
.header(maud::html! { span { "Welcome" } })
.child(maud::html! { p { "Body" } })
.footer(maud::html! { button { "Save" } })
.action(maud::html! { button { "Edit" } })
.render()
}What this gives you:
- Bon-backed typed builders
Slot<Markup>andSlot<Vec<Markup>>as the slot declaration path#[mx(default)]for the single default slot- explicit colocated
fn css() -> Markupandfn js() -> Markuphelpers - a normal
impl Renderwhere you place(Self::css())/(Self::js())exactly where they belong - builder
.render()just renders the completed component value
Current constraints:
- use
Slot<T>/Slot<Vec<T>>for slot declarations - reserve
#[mx(default)]for selecting the default slot only - use Rust
DefaultorOption<T>for non-slot defaults - if there are multiple slot fields, mark exactly one
#[mx(default)] - repeated slots use
Slot<Vec<T>>plus#[mx(each = item_name)]
Mental model:
#[derive(Component)]owns fields, slots, and the Bon-backed builder- inherent
css()/js()helpers own component-local assets - the
Renderimpl stays explicit and decides where those helpers are emitted
This keeps the builder story ergonomic while leaving rendering explicit and easy to reason about.
The normal path is just .render() on the complete builder. Use .build()
only when you specifically want the concrete component value first.
let markup = Card::new()
.title("Profile")
.child(maud::html! { p { "Body" } })
.render();The current CSS/JS/component story is designed to layer on top of a few small browser-side tools:
- Surreal for DOM ergonomics around
me()/any()style behavior - css-scope-inline for colocated scoped CSS transforms
- Preact Signals for the signals runtime surface the crate builds on
maud-extensions is not trying to replace those pieces; it is trying to make
them feel coherent and component-local from Maud.
To bootstrap the browser-side runtime in a page, use mx::Init in <head>:
use maud::html;
use mx::Init;
fn page() -> maud::Markup {
html! {
head {
(Init::all())
}
body {
// component markup here
}
}
}Recommended bootstrap entrypoints:
Init::all()Init::new().surrealjs().scoped_css().signals().build()
If you want a more minimal component style, you can still stop at plain
html! + css! + js!. The component system is intentionally a second layer,
not the only way to use the crate.
When reuse helps, define local helper functions with Rust identifiers:
use maud::html;
use maud_extensions::{css, js};
css!(card_css, {
me { gap: px!(12); }
});
js!(card_js, once, {
me().class_add("ready");
});
fn card() -> maud::Markup {
html! {
article.card {
(card_css())
(card_js())
"Hello"
}
}
}Supported css! forms:
css! { ... }css!(name, { ... })
Supported js! forms:
js! { ... }js!(once, { ... })js!(name, { ... })js!(name, once, { ... })
Inside css! token mode you can use:
raw!(r#"..."#)media!(prelude, { ... })container!(prelude, { ... })supports!(prelude, { ... })layer!(prelude, { ... })keyframes!(prelude, { ... })- unit helpers:
rem!(...)em!(...)px!(...)pct!(...)vw!(...)vh!(...)ms!(...)s!(...)
Example:
use maud_extensions::css;
fn responsive_styles() -> maud::Markup {
css! {
media!("(min-width: 48rem)", {
me { padding: rem!(2); }
})
supports!("(display: grid)", {
me { gap: px!(12); }
})
}
}css!andjs!are placement-sensitive local emittersjs!(once, ...)relies on adata-mx-js-ranmarker on the parent element- CSS token mode only sees Rust-tokenizable input; use
raw!(...)for arbitrary CSS fragments - JavaScript is validated with SWC before emission
- CSS is checked for lightweight syntax and raw-text safety before emission