Tailwind CSS
bext integrates with Tailwind CSS v4 for build-time CSS generation. The @bext-stack/framework/tailwind module scans your source files for class candidates and generates a production-ready CSS file.
How it works
- Your build script calls
buildCSS()with source file patterns - The Tailwind compiler loads your CSS entry (defaults to
@import "tailwindcss") - Source files are scanned for class-like tokens (class candidates)
- Only the CSS for classes actually used in your code is generated
- The output CSS file is written to disk and referenced in your templates
No PostCSS config file is needed. No tailwind.config.js. Tailwind v4 uses CSS-first configuration.
Installation
bun add tailwindcss
Build script
Create a build script that generates CSS before the SSR bundle:
// scripts/build.ts
import { buildCSS } from "@bext-stack/framework/tailwind";
const result = await buildCSS({
sources: ["server"], // Directories to scan for class candidates
outFile: "dist/styles.css", // Where to write the generated CSS
});
console.log(`Tailwind CSS built: ${result.size} bytes`);
Run it:
bun run scripts/build.ts
Full options
interface TailwindBuildOptions {
/** CSS entry content or path. Defaults to '@import "tailwindcss";' */
css?: string;
/** Glob patterns or directories to scan for class candidates. */
sources: string[];
/** Output file path for the generated CSS. */
outFile: string;
}
Custom CSS entry
You can pass a custom CSS string with Tailwind directives, theme customizations, and additional CSS:
await buildCSS({
css: `
@import "tailwindcss";
@theme {
--color-primary: #06b6d4;
--color-accent: #f43f5e;
--font-sans: "Inter", system-ui, sans-serif;
}
/* Custom utilities */
.prose-custom h1 {
@apply text-3xl font-bold tracking-tight;
}
`,
sources: ["server"],
outFile: "dist/styles.css",
});
How candidate scanning works
The scanner extracts all class-like tokens from your source files using a broad regex pattern. It matches sequences of letters, digits, hyphens, colons, slashes, dots, and brackets — covering all Tailwind class patterns:
- Standard classes:
bg-white,text-sm,p-4 - Responsive prefixes:
md:flex,lg:grid-cols-3 - State variants:
hover:bg-blue-600,focus:ring-2 - Arbitrary values:
w-[200px],bg-[#1a1a1a] - Negative values:
-mt-4,-translate-x-1/2
The scanner is deliberately over-inclusive. Tokens that do not match any Tailwind utility are simply ignored — they produce no output CSS.
Scanned file types
The scanner processes files with these extensions:
.ts,.tsx— TypeScript source and JSX.js,.jsx— JavaScript source.html— HTML templates.css— Other CSS files (for@applydirectives)
Directories named node_modules, .git, and dist are automatically skipped.
stylesheet() helper
Use the stylesheet() helper to generate a <link> tag in your templates:
import { stylesheet } from "@bext-stack/framework";
function renderPage(page: Page, ctx: RenderContext): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
${stylesheet("/styles.css")}
</head>
<body>
${page.html}
</body>
</html>`;
}
This generates:
<link rel="stylesheet" href="/styles.css">
You can pass additional attributes:
stylesheet("/styles.css", { media: "screen", crossorigin: "anonymous" })
// <link rel="stylesheet" href="/styles.css" media="screen" crossorigin="anonymous">
Tailwind in JSX components
With bext's JSX runtime, use className (automatically aliased to class):
function Card({ title, body }: { title: string; body: string }) {
return (
<div className="rounded-xl border border-gray-200 p-6 hover:shadow-lg transition-shadow">
<h3 className="text-lg font-semibold text-gray-900 mb-2">{title}</h3>
<p className="text-sm text-gray-600 leading-relaxed">{body}</p>
</div>
);
}
function HeroSection() {
return (
<section className="py-24 px-6 text-center bg-gradient-to-b from-cyan-50 to-white">
<h1 className="text-5xl font-bold tracking-tight text-gray-900 mb-4">
Build faster with bext
</h1>
<p className="text-xl text-gray-600 max-w-2xl mx-auto mb-8">
A single binary that replaces your entire deployment stack.
</p>
<div className="flex gap-4 justify-center">
<a href="/get-started" className="px-6 py-3 bg-cyan-500 text-white rounded-lg font-medium hover:bg-cyan-600">
Get Started
</a>
<a href="/docs" className="px-6 py-3 border border-gray-300 rounded-lg font-medium hover:bg-gray-50">
Documentation
</a>
</div>
</section>
);
}
Or in template literals:
function renderPage(page: Page): string {
return `
<div class="max-w-4xl mx-auto px-6 py-12">
<h1 class="text-3xl font-bold mb-4">${page.title}</h1>
<div class="prose prose-gray">${page.html}</div>
</div>
`;
}
Example build script with bext config
A typical bext.config.toml with a build step:
[build]
script = "scripts/build-ssr.ts"
watch_dirs = ["server"]
And the build script:
// scripts/build-ssr.ts
import { buildCSS } from "@bext-stack/framework/tailwind";
// 1. Build Tailwind CSS
const css = await buildCSS({
sources: ["server"],
outFile: "public/styles.css",
});
console.log(`CSS: ${css.size} bytes`);
// 2. Bundle the SSR entry (Bun builds this into a single file for the V8 pool)
const result = await Bun.build({
entrypoints: ["server/entry.ts"],
outdir: "dist",
target: "bun",
minify: true,
external: [],
});
if (!result.success) {
console.error("Build failed:", result.logs);
process.exit(1);
}
console.log("SSR bundle built");
Dark mode
Tailwind v4 supports dark mode out of the box. Use the dark: variant:
function ThemeCard({ title }: { title: string }) {
return (
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-6">
<h3 className="text-gray-900 dark:text-gray-100 font-semibold">{title}</h3>
</div>
);
}
PRISM sites on bext use the Rust-native CSS engine (see below) — you do not need the
buildCSS() TypeScript script at all. The route-css feature is compiled in by default and
generates CSS at startup without spawning Bun.
Rust-native CSS engine
bext also includes a Rust-native Tailwind v4 compiler that replaces the TypeScript pipeline entirely. When the route-css feature is compiled in (default), bext:
- Builds
public/styles.cssat startup without spawning Bun - Generates per-route minimal CSS after SSR (inline
<style>with only used classes) - Rebuilds CSS in ~1ms when only
.cssfiles change (no Bun needed)
See Rust CSS Engine for full details, CLI commands, and theme reference.
Tips
- Scan the right directories. Only scan directories that contain class names. Scanning
node_modulesis unnecessary and slow. - Use
@themefor custom colors. Tailwind v4 uses CSS custom properties for theming instead of a JavaScript config file. - Arbitrary values work. Classes like
w-[calc(100%-2rem)]andbg-[#1a1a1a]are extracted correctly. - Build once, serve many. The generated CSS is a static file. bext serves it directly from disk with proper caching headers. No per-request CSS generation.
Related
- Rust CSS Engine — full reference for the Rust-native Tailwind v4 compiler
- PRISM Framework — the JSX framework whose
classNameusage the scanner reads - JSX Runtime — how
classNameis aliased toclassin HTML output - Template Framework — the
stylesheet()helper and build script wiring - Live Reload — CSS change detection and fast rebuild path