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 when cond evaluates to true.
  • expr if cond else alt — Includes expr when cond is true, otherwise includes alt.

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 nested Class values all treat empty strings and None as absent.
  • fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) — Writes the entry into the output buffer when is_present() returns true.

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, a Vec, and a conditional branch are seamlessly combined via the ClassViewParts trait.
  • Example 3 illustrates nesting where class!("card", class!("btn", "btn-lg")) renders as class="card btn btn-lg" with correct spacing handled by ClassWriter.
  • Example 4 results in <p>No class attribute</p> because both entries are absent, triggering the omission logic in the Class attribute implementation.

Summary

  • Declarative syntax — Use expr if cond and expr if cond else alt for conditional classes.
  • Trait-based extensibility — Any type implementing ClassViewParts (strings, Options, Vectors, tuples) works in class!.
  • Zero-allocation composition — ClassWriter streams entries directly into the view buffer without intermediate strings.
  • Smart attribute omission — When all entries are absent, Topcoat omits the class attribute entirely rather than rendering an empty value.
  • Mixed-type branches — The ClassBranch<A, B> enum enables if/else with 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →