Rust CSS Engine
bext includes a Rust-native Tailwind CSS v4 compiler powered by encre-css. It replaces the TypeScript Tailwind pipeline with a pure-Rust implementation that runs inside the server process — no Node.js or Bun subprocess needed for CSS.
Why
The traditional Tailwind build pipeline requires spawning Bun to run the tailwindcss npm package. This works but adds latency to builds, requires a JS runtime for CSS, and makes CSS-only changes trigger full bundle rebuilds.
The Rust CSS engine eliminates all of that:
| TypeScript Tailwind | Rust CSS Engine | |
|---|---|---|
| CSS build time | ~200-500ms (Bun subprocess) | ~1-30ms (in-process) |
| CSS-only file change | Full Bun rebuild | Rust rebuild, no Bun |
| Per-route CSS | Not available | Automatic (inline <style>) |
| Runtime dependency | Bun + tailwindcss npm | None (compiled into binary) |
| Dev mode hot reload | File watcher + Bun | File watcher + Rust |
Enabling
The route-css feature is included in default builds. No configuration needed — it works automatically.
To verify it's compiled in:
bext version
# Look for "route-css" in the feature flags
To explicitly disable it:
cargo build -p bext-server --release \
--no-default-features \
--features v8,tls,nginx-compat
How it works
At startup
When bext initializes a PRISM site, it builds public/styles.css from Rust before the render pool starts:
PRISM init → bext_css::build_site_css() → scan src/**/*.{tsx,ts,jsx,css,html}
→ extract class candidates → encre-css generate → write public/styles.css
The TypeScript build script automatically skips its own Tailwind step (via BEXT_RUST_CSS=1 env var).
Per-route CSS
After every SSR render, bext scans the rendered HTML for class="..." attributes and generates a <style data-bext-css> block containing only the Tailwind utilities used on that specific page. This is injected before </head>.
<!-- Before: generic styles.css with everything -->
<link rel="stylesheet" href="/styles.css" />
<!-- After: also includes per-route minimal CSS -->
<link rel="stylesheet" href="/styles.css" />
<style data-bext-css>
.flex { display: flex; }
.items-center { align-items: center; }
.gap-4 { gap: 1rem; }
/* only classes used on THIS page */
</style>
This works on both rendering paths:
- On-demand mode: after renderToString() returns HTML
- Pre-built mode: after __fetch returns an HTML response
CSS-only live reload
When the file watcher detects that only .css files changed (no .ts/.tsx), it takes a fast path:
1. Skips Bun entirely
2. Calls bext_css::build_site_css() in Rust (~1ms)
3. Clears the ISR response cache
4. Sends HMR css-update to connected browsers
In dev mode, the HMR client hot-swaps both <link> stylesheets and inline <style data-bext-css> blocks without a page reload.
CLI
Build CSS for one site
bext css sites/cloud
Build CSS for all sites
bext css sites/ --all
Output:
Building CSS for all sites in sites/
main 12.4 KB
registry 8.6 KB
learn 8.4 KB
status 8.4 KB
companion-site 8.3 KB
blog 8.1 KB
docs 7.6 KB
cloud 5.9 KB
8 sites, 66.8 KB total, 280ms
Build with bext build
CSS is built automatically before the JS bundle:
bext build
# css 12.4 KB (Rust)
# Build complete in 1200ms (bundle: .bext/production.js)
Skip CSS if you want to use an existing styles.css:
bext build --no-css
Theme
The bext design tokens are compiled into the engine:
Colors
| Token | Value | Usage |
|---|---|---|
bext-black |
#0a0a0a |
bg-bext-black, text-bext-black |
bext-white |
#fafafa |
bg-bext-white, text-bext-white |
bext-accent-2 |
#10b981 |
bg-bext-accent-2, text-bext-accent-2 |
bext-error |
#ef4444 |
text-bext-error, border-bext-error |
bext-warning |
#f59e0b |
text-bext-warning |
bext-success |
#10b981 |
text-bext-success |
bext-info |
#3b82f6 |
text-bext-info |
bext-cloud |
#8b5cf6 |
Site accent: Cloud dashboard |
bext-registry |
#f59e0b |
Site accent: Plugin registry |
bext-learn |
#10b981 |
Site accent: Learn platform |
bext-companion |
#6366f1 |
Site accent: Companion app |
Fonts
- Sans: "Inter", system-ui, -apple-system, sans-serif
- Mono: "JetBrains Mono", "Fira Code", monospace
Dark mode
Class-based dark mode using .dark selector:
<div className="bg-white dark:bg-gray-900 text-black dark:text-white">
Adapts to dark mode
</div>
Responsive breakpoints
All standard Tailwind v4 breakpoints are available:
<div className="hidden md:flex lg:grid lg:grid-cols-3">
Responsive layout
</div>
CSS custom properties
The engine also outputs CSS custom properties (design tokens) for use in hand-written CSS:
:root {
--bext-black: #0a0a0a;
--bext-white: #fafafa;
--bext-accent: #3b82f6;
--bg: var(--bext-white);
--text: var(--bext-black);
--border: #e5e7eb;
--surface: #f9fafb;
}
:root.dark {
--bg: #111827;
--text: #f3f4f6;
--border: #374151;
--surface: #1f2937;
}
Use them in component styles:
.card {
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
}
Architecture
The CSS engine lives in crates/bext-css/ and depends on encre-css (MIT, Tailwind v4 compatible). Key functions:
| Function | Purpose |
|---|---|
bext_css::config() |
Shared theme config (initialized once via OnceLock) |
bext_css::build_site_css(path) |
Build public/styles.css for a site |
bext_css::build_all_sites(path) |
Batch build for all sites in a directory |
bext_css::inject_route_css(html) |
Extract classes from HTML, inject <style> |
bext_css::css_for_html(html) |
Generate CSS for rendered HTML (no injection) |
bext_css::is_css_only_change(files) |
Classify file changes for the watcher fast path |
bext_css::rebuild_if_changed(path) |
Mtime-aware rebuild (skip if up-to-date) |
bext_css::content_hash(css) |
FNV-1a hash for cache busting |
Compatibility with TypeScript Tailwind
Both engines can coexist. When the Rust engine is active, it sets BEXT_RUST_CSS=1 before running Bun build scripts. The TypeScript buildCSS() and ensureCSS() functions check this variable and skip their work:
// In build.ts / serve.ts — automatically skipped
if (process.env.BEXT_RUST_CSS === "1") {
console.log("css (handled by bext-server Rust engine)");
return;
}
To force the TypeScript pipeline (e.g., for debugging), unset the variable:
BEXT_RUST_CSS=0 bext build