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
tailwindCargo 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 totopcoat::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:
- 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 - Input generation – Creates a minimal CSS file containing
@import "tailwindcss"(or uses your custom input file) - Class extraction – Executes
tailwindcss -i <input> -o $OUT_DIR/tailwind.css --cwd $CARGO_MANIFEST_DIR --minify, which scans Rust source files for literalclass="..."values insideview!macros - 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.
Link the Stylesheet in Your Layout
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
tailwindfeature in both[dependencies]and[build-dependencies]withinCargo.toml - Create a
build.rsfile callingtopcoat::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 inview!macros - Customize behavior by providing custom input CSS files, specifying alternative CLI binaries, or pinning specific Tailwind versions via the
BuildConfigAPI
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →