maud-extensions
Small, local superpowers for Maud.
Install
Recommended: install the crate as mx so the macro and component surface reads
well at call sites:
If you want the experimental component system too, enable components on the
same dependency:
[]
= { = "maud-extensions", = "0.6.7", = ["components"] }
Core Story
Write plain html! and emit local CSS and JS where they belong:
use html;
use ;
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
Experimental Components
The component system is opt-in behind the components feature.
Preferred authoring pattern:
use Markup;
use ;
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 = new
.title
.child
.render;
Bundled browser-side building blocks
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 html;
use Init;
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.
Named Helpers
When reuse helps, define local helper functions with Rust identifiers:
use html;
use ;
css!;
js!;
Supported css! forms:
css! { ... }css!(name, { ... })
Supported js! forms:
js! { ... }js!(once, { ... })js!(name, { ... })js!(name, once, { ... })
CSS Helper Macros
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 css;
Limits
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