# maud-extensions
[](https://crates.io/crates/maud-extensions)
[](https://docs.rs/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:
```bash
cargo add maud-extensions --rename mx
```
If you want the experimental component system too, enable `components` on the
same dependency:
```toml
[dependencies]
mx = { package = "maud-extensions", version = "0.6.7", features = ["components"] }
```
## Core Story
Write plain `html!` and emit local CSS and JS where they belong:
```rust
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
## Experimental Components
The component system is opt-in behind the `components` feature.
Preferred authoring pattern:
```rust
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>` and `Slot<Vec<Markup>>` as the slot declaration path
- `#[mx(default)]` for the single default slot
- explicit colocated `fn css() -> Markup` and `fn js() -> Markup` helpers
- a normal `impl Render` where 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 `Default` or `Option<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 `Render` impl 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.
```rust
let markup = Card::new()
.title("Profile")
.child(maud::html! { p { "Body" } })
.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](https://github.com/gnat/surreal) for DOM ergonomics around `me()` /
`any()` style behavior
- [css-scope-inline](https://github.com/gnat/css-scope-inline) for colocated
scoped CSS transforms
- [Preact Signals](https://github.com/preactjs/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>`:
```rust
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.
## Named Helpers
When reuse helps, define local helper functions with Rust identifiers:
```rust
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, { ... })`
## 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:
```rust
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); }
})
}
}
```
## Limits
- `css!` and `js!` are placement-sensitive local emitters
- `js!(once, ...)` relies on a `data-mx-js-ran` marker 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