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

1. Your build script calls buildCSS() with source file patterns 2. The Tailwind compiler loads your CSS entry (defaults to @import "tailwindcss") 3. Source files are scanned for class-like tokens (class candidates) 4. Only the CSS for classes actually used in your code is generated 5. 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


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 @apply directives)

Directories named node_modules, .git, and dist are automatically skipped.

stylesheet() helper

Use the stylesheet() helper to generate a <link> tag in your templates:



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


// 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>
  );
}

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.css at startup without spawning Bun

- Generates per-route minimal CSS after SSR (inline <style> with only used classes)

- Rebuilds CSS in ~1ms when only .css files 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_modules is unnecessary and slow.

- Use @theme for 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)] and bg-[#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.