How to Implement Conditional CSS Class Lists with Topcoat's class! Macro
Topcoat's class! macro enables type-safe, zero-allocation construction of CSS class strings using conditional syntax like "active" if is_enabled, automatically omitting empty values and the entire attribute when no classes are present.
The class! macro in the tokio-rs/topcoat repository provides a declarative DSL for assembling HTML class attributes within Rust view templates. By implementing the ClassViewParts trait for diverse types including strings, options, vectors, and nested classes, the macro allows developers to compose complex conditional class lists without intermediate string allocations.
Conditional Syntax Overview
The macro accepts a comma-separated list of entries where each item follows one of three patterns documented in [crates/topcoat-view/macro/docs/class.md](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-view/macro/docs/class.md):
expr— Always included in the final string.expr if cond— Included only whencondevaluates totrue.expr if cond else alt— Includesexprwhencondistrue, otherwise includesalt.
This syntax allows for expressive, declarative class construction directly inside Topcoat's view! macro.
Core Architecture: Traits and Writers
The ClassViewParts Trait
According to the implementation in [crates/topcoat-view/src/class.rs](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-view/src/class.rs), every entry in a class! invocation must implement the ClassViewParts trait. This trait defines two critical methods:
fn is_present(&self) -> bool— Determines whether the entry contributes to the output. Implementations for&str,String,Option<T>,Vec<T>, arrays, tuples, and nestedClassvalues all treat empty strings andNoneas absent.fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>)— Writes the entry into the output buffer whenis_present()returnstrue.
The ClassWriter Implementation
The ClassWriter struct (lines 56-74 in class.rs) manages the actual string construction. It ensures that:
- Present entries are separated by exactly one space.
- Absent entries produce no separator.
- No leading or trailing whitespace appears in the final attribute value.
Mixed-Type Conditionals with ClassBranch
When using the if/else syntax with branches of different types, the macro lowers them to the hidden enum ClassBranch<A, B> (lines 23-33 of class.rs). This allows mixed-type conditionals without requiring extra heap allocation or boxing, preserving the macro's zero-allocation guarantees.
Automatic Omission of Empty Classes
The Class struct tracks whether any entry is present through the is_present() method. When the entire list contains no present entries—such as when all conditionals evaluate to false or all options are None—the attribute_present method returns false (lines 29-33 of class.rs). This signals Topcoat to omit the entire class attribute from the rendered HTML element rather than emitting class="".
Practical Code Examples
The following examples demonstrate the full capabilities of the class! macro:
use topcoat::view::{class, view};
#[topcoat::view::component]
async fn example() -> topcoat::Result {
let is_active = true;
let variant: Option<&str> = Some("primary");
let sizes = vec!["px-4".to_owned(), "py-2".to_owned()];
let enabled = false;
// Static classes with simple conditional
view! {
<button class=(class!("btn", "btn-lg", "active" if is_active))>
"Save"
</button>
}
// Combining Options, Vectors, and if/else branches
view! {
<button class=(class!(
"btn",
variant, // Option<&str>, omitted if None
sizes, // Vec<String>, each entry added
"cursor-pointer" if enabled else "opacity-50"
))>
"Save"
</button>
}
// Nested class composition
view! {
<div class=(class!("card", class!("btn", "btn-lg")))>{"Content"}</div>
}
// Attribute completely omitted when all entries absent
view! {
<p class=(class!(Option::<&str>::None, ""))>{"No class attribute"}</p>
}
}
Key behaviors illustrated:
- Example 1 shows basic conditional inclusion using
"active" if is_active. - Example 2 demonstrates heterogeneous composition: an
Option, aVec, and a conditional branch are seamlessly combined via theClassViewPartstrait. - Example 3 illustrates nesting where
class!("card", class!("btn", "btn-lg"))renders asclass="card btn btn-lg"with correct spacing handled byClassWriter. - Example 4 results in
<p>No class attribute</p>because both entries are absent, triggering the omission logic in theClassattribute implementation.
Summary
- Declarative syntax — Use
expr if condandexpr if cond else altfor conditional classes. - Trait-based extensibility — Any type implementing
ClassViewParts(strings, Options, Vectors, tuples) works inclass!. - Zero-allocation composition —
ClassWriterstreams entries directly into the view buffer without intermediate strings. - Smart attribute omission — When all entries are absent, Topcoat omits the
classattribute entirely rather than rendering an empty value. - Mixed-type branches — The
ClassBranch<A, B>enum enablesif/elsewith different types without boxing.
Frequently Asked Questions
How does the class! macro handle optional classes?
The macro treats Option<T> as an optional entry. When the variant is Some, the contained value is included; when None, the entry is skipped entirely. This logic is implemented via ClassViewParts for Option<T> in crates/topcoat-view/src/class.rs, where is_present() returns true only for Some variants.
What happens when all class entries evaluate to absent?
When every entry in the class! list is absent—whether through None values, empty strings, or false conditionals—the Class value reports attribute_present as false. Topcoat then omits the entire class attribute from the rendered HTML element, resulting in cleaner markup without empty attributes.
Can I use different types in the if and else branches of a conditional?
Yes. The macro supports mixed-type conditionals like "enabled" if active else "disabled" where the branches have different types. The implementation uses the ClassBranch<A, B> enum to unify the types without requiring allocation, as defined in lines 23-33 of class.rs.
How do I combine multiple dynamic class sources?
You can pass any combination of static strings, Option<T>, Vec<T>, arrays, tuples, and nested Class values to the macro. Each type implements ClassViewParts, allowing heterogeneous lists like class!("base", maybe_variant, vec_of_sizes, nested_class). The ClassWriter ensures proper spacing between all present entries.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →