Docs
Class composition
Compose Tailwind classes with static strings, ClassList, and optional caller classes.
Proa composes hot-path classes from static string fragments instead of building a String at render time. This page covers static class constants, ClassList, capacity, and the allocation to avoid.
Declaring static class constants
Keep repeated class sets in constants:
pub const CARD_BASE: &str = "rounded-lg border bg-card text-card-foreground shadow-sm";
pub const CARD_COMPACT: &str = "p-3 text-sm";
pub const CARD_SPACIOUS: &str = "p-6";
Tailwind-heavy component recipes often use a local tw_join! helper. It joins static class fragments at compile time into one &'static str.
use crate::tw_join;
pub const BUTTON_BASE: &str = tw_join!(
"inline-flex",
"items-center",
"justify-center",
"rounded-md",
"text-sm",
"font-medium",
);
Constants cover the fragments you know at compile time. To pick between them per render, combine them with ClassList.
Composing with ClassList
A ClassList<N> stores references to static class strings and writes them with spaces between segments. It does not allocate a String.
use proa_core::ClassList;
pub enum CardDensity {
Compact,
Spacious,
}
impl CardDensity {
pub const fn classes(self) -> &'static str {
match self {
Self::Compact => CARD_COMPACT,
Self::Spacious => CARD_SPACIOUS,
}
}
}
pub fn card_classes(density: CardDensity, class: Option<&'static str>) -> ClassList<3> {
ClassList::new()
.add(CARD_BASE)
.add(density.classes())
.add_opt(class)
}
Use the ClassList directly in a class={...} attribute:
html_sync! {
<section class={card_classes(self.density, self.class)}>
{self.children}
</section>
}
One number in that signature carries a hard constraint: the 3.
Choosing a capacity
The N in ClassList<N> is the maximum number of class segments:
ClassList::<4>::new()
.add("base")
.add("variant")
.add_if(disabled, "opacity-50")
.add_opt(class)
If you add more than N segments, ClassList panics. Pick the exact capacity for the call site and count the optional segments too.
Sizing the list correctly matters only if you use it. The alternative costs an allocation on every render.
Avoiding hot-path allocation
Avoid this in render code:
let classes = format!("{} {}", CARD_BASE, self.density.classes());
Prefer ClassList:
let classes = ClassList::<3>::new()
.add(CARD_BASE)
.add(self.density.classes())
.add_opt(self.class);
If a class string comes from user input or application data, validate or normalize it before rendering. Most component APIs accept Option<&'static str> for extension classes, which keeps common render paths static.
Next steps
- HTML rendering
- Put the composed classes into a
class={...}attribute.
- Put the composed classes into a
- Writing performant pages
- Find the other allocations a render path can hide.
- Escaping and raw HTML
- See how Proa escapes attribute values it writes.
- Tailwind
- Get editor completion for class strings inside Rust constants.