How to Configure Topcoat with Tailwind CSS Integration

Topcoat bundles a standalone Tailwind CSS CLI wrapper that compiles your styles at build time via a Cargo build script, eliminating the need for Node.js or external asset pipelines.

Configuring Topcoat with Tailwind CSS integration allows you to generate optimized stylesheets directly within Rust's build process. The tokio-rs/topcoat crate provides this functionality through an opt-in tailwind feature that downloads the Tailwind CLI, scans your view! macros for utility classes, and serves the resulting CSS as a content-hashed asset. This approach keeps your build entirely self-contained while providing full access to Tailwind's just-in-time compiler.

How the Tailwind Integration Works

Architecture Overview

The integration centers on the topcoat::tailwind module, implemented in [crates/topcoat/src/tailwind.rs](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat/src/tailwind.rs). When enabled, Topcoat acts as a thin wrapper around the official Tailwind CLI, handling binary management and asset pipeline integration automatically.

The system relies on three core components:

  • Feature-gated compilation – The tailwind Cargo feature must be enabled for both runtime and build dependencies
  • Build script execution – BuildConfig::new().render() orchestrates CLI download, CSS generation, and file output
  • Asset macro expansion – tailwind::stylesheet!() resolves to topcoat::asset::asset!(concat!(env!("OUT_DIR"), "/tailwind.css")), integrating with Topcoat's existing asset serving infrastructure

Build-Time Execution Flow

When you invoke BuildConfig::new().render() in your build.rs, the following occurs:

  1. Binary acquisition – If not present in OUT_DIR, the build script downloads the Tailwind CLI (pinned to version 4.3.2) for your target platform
  2. Input generation – Creates a minimal CSS file containing @import "tailwindcss" (or uses your custom input file)
  3. Class extraction – Executes tailwindcss -i <input> -o $OUT_DIR/tailwind.css --cwd $CARGO_MANIFEST_DIR --minify, which scans Rust source files for literal class="..." values inside view! macros
  4. Asset registration – The generated CSS becomes available via the stylesheet!() macro, receiving automatic content hashing and cache-busting headers

The scanning process respects your .gitignore file, automatically excluding target/ and other build artifacts from analysis. Notably, the implementation does not parse view! macros itself; it delegates class detection entirely to Tailwind's native CLI.

Step-by-Step Configuration

Enable the Tailwind Feature

Add the tailwind feature to both your runtime dependencies and build dependencies in Cargo.toml:

[dependencies]
topcoat = { version = "0.3.1", features = ["tailwind"] }

[build-dependencies]
topcoat = { version = "0.3.1", default-features = false, features = ["tailwind"] }

The build dependency disables default features to minimize compile times while retaining Tailwind functionality.

Create the Build Script

Create a build.rs file in your crate root that invokes the build configuration:

fn main() {
    // Generate Tailwind CSS at compile time
    topcoat::tailwind::BuildConfig::new()
        .render()
        .expect("Tailwind build failed");
}

This default configuration downloads the Tailwind CLI if necessary, generates an input file containing @import "tailwindcss", and writes the compiled output to $OUT_DIR/tailwind.css.

Use the tailwind::stylesheet!() macro inside your layout's HTML head:

use topcoat::{
    router::{Slot, layout},
    tailwind,
    view::view,
};

#[layout]
async fn layout(slot: Slot<'_>) -> topcoat::Result {
    view! {
        <!DOCTYPE html>
        <html>
            <head>
                <link rel="stylesheet" href=(tailwind::stylesheet!())>
            </head>
            <body>
                (slot.await?)
            </body>
        </html>
    }
}

The macro expands to a call to topcoat::asset::asset!(), ensuring the stylesheet receives a content-hashed URL and proper cache headers.

Customization Options

Custom Input CSS

To use custom Tailwind configurations or import additional CSS, provide a path to your own input file:

fn main() {
    topcoat::tailwind::BuildConfig::new()
        .input("src/styles/app.css")   // Path relative to Cargo.toml
        .render()
        .expect("Tailwind build failed");
}

Create src/styles/app.css with your custom directives:

@import "tailwindcss";

@theme {
  --font-sans: Inter, sans-serif;
}

Using a Pre-Installed Binary

For offline builds or specific Tailwind versions already installed on your system, bypass the automatic download:

fn main() {
    topcoat::tailwind::BuildConfig::new()
        .executable("tailwindcss")        // Assumes binary is on $PATH
        .render()
        .expect("Tailwind build failed");
}

Alternatively, read the executable path from an environment variable:

fn main() {
    topcoat::tailwind::BuildConfig::new()
        .executable_env("TAILWIND_CLI")   // Reads path from env var
        .render()
        .expect("Tailwind build failed");
}

Summary

  • Enable the tailwind feature in both [dependencies] and [build-dependencies] within Cargo.toml
  • Create a build.rs file calling topcoat::tailwind::BuildConfig::new().render() to trigger CSS generation during compilation
  • Include the stylesheet via tailwind::stylesheet!() in your layout's <head> section
  • Store generated CSS in $OUT_DIR/tailwind.css, automatically scanned for utility classes found in view! macros
  • Customize behavior by providing custom input CSS files, specifying alternative CLI binaries, or pinning specific Tailwind versions via the BuildConfig API

Frequently Asked Questions

Do I need Node.js installed to use Tailwind with Topcoat?

No. Topcoat's integration downloads and executes the standalone Tailwind CSS CLI binary (version 4.3.2 by default) automatically during the Cargo build process. This binary operates independently of Node.js, npm, or any JavaScript runtime, making the build pipeline entirely Rust-native.

How does Topcoat detect Tailwind classes in my Rust code?

Class detection relies entirely on the Tailwind CLI's built-in scanner, not Topcoat's macro parser. The CLI scans all files in your crate root (respecting .gitignore) for literal class="..." attributes, including those inside Topcoat's view! macro invocations. This ensures compatibility with standard Tailwind purging behavior while keeping the integration lightweight.

Can I use a specific version of the Tailwind CLI?

Yes. While the default implementation pins version 4.3.2, you can specify an alternative version using the .version() method on BuildConfig, or provide a pre-installed binary path via .executable() or .executable_env(). Note that you must ensure version compatibility with your tailwind.config.js or CSS theme directives.

Why isn't my CSS regenerating when I change my markup?

BuildConfig::render() does not emit explicit cargo:rerun-if-* directives, relying instead on Cargo's default behavior. The build script reruns whenever any non-ignored file in your package changes. If you are modifying files outside the standard source tree or if your .gitignore excludes relevant template files, Cargo may not detect the change. Ensure your markup files are part of the package and not excluded by ignore patterns.

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 →