Tailwind Setup

Reepolee uses Tailwind v4 for styling. There is no PostCSS pipeline, no plugin registry, and no JavaScript-side configuration file - Tailwind v4 reads its configuration from CSS itself. The CLI scans your source files, picks up the class names you use, and produces a CSS file with only those utilities included.

The entry point is css/app.css. The CLI compiles it to static/app.css for production and static/app-dev.css during development. The layout loads whichever one matches the current mode.

This page covers the setup, the build pipeline, and how to extend it. Theming, form defaults, and page transitions each have their own page in this section.

The Entry Point

css/app.css starts with the Tailwind import. Tailwind v4 discovers project sources through its default scan; the rest of the file contains the project's design tokens, base styles, components, utilities, and imported stylesheets:

@import "tailwindcss";

@theme {
    /* design tokens - see Theming & Tokens */
}

@layer base {
    /* element defaults */
}

@layer components {
    /* reusable patterns */
}

@import "./forms.css";
@import "./toasts.css";
@import "./filters.css";
@import "./markdown-editor.css";

/* @import "./transitions.css"; */

@utility primary {
    /* ... */
}

If a project adds a directory that Tailwind's default source detection does not discover, add an explicit @source directive for that directory. Keep the directive close to the import at the top of app.css so the scan configuration has one obvious home.

Building CSS

Two scripts cover the lifecycle:

bun run css:watch    # development - rebuilds on every change
bun run css:once     # one development build, then exits
bun run css:build    # production - writes a minified bundle

The watcher writes to static/app-dev.css and stays running. The production build writes to static/app.css and exits.

The scripts call the Tailwind v4 CLI (installed via bun run get:tw as a standalone binary to avoid npm collision issues). The full commands behind the scripts:

tailwindcss -i ./css/app.css -o ./static/app-dev.css --watch
tailwindcss -i ./css/app.css -o ./static/app-dev.css
tailwindcss -i ./css/app.css -o ./static/app.css --minify

bun dev runs the watcher alongside the hot-reload server through the dev orchestrator (scripts/dev_run.ts), so editing a template and seeing the new utility show up in the browser is a single change away.

How the Layout Picks the Right File

The layout template references the stylesheet conditionally based on whether the server is in dev mode:

<link rel="stylesheet" href='/app{~ props.is_dev ? "-dev" : "" }.css?v={= props.version }' />

In development, props.is_dev is true and the layout loads /app-dev.css. In production it's false and the layout loads /app.css. The ?v={= props.version } query parameter is a cache-buster - in production it's the version from package.json (so cached stylesheets are invalidated on deploy), in development it's a timestamp (so every reload picks up the freshest CSS).

Source Scanning

Tailwind v4 scans files for class names by reading the actual text - it doesn't parse REE or JS in any structured way, so anything that looks like a class name is picked up. A useful side effect: TypeScript files can declare class names as string literals and they're included in the output:

const status_colors = {
    pending: "bg-yellow-100 text-yellow-800",
    active: "bg-green-100 text-green-800",
    failed: "bg-red-100 text-red-800",
};

Tailwind sees bg-yellow-100, text-yellow-800, etc. and includes them.

The flip side: class names assembled at runtime won't work. bg-${color} produces a string Tailwind can't see, so the class isn't in the output and the browser ignores it. The fix is to enumerate the possible values explicitly - a lookup object like the one above, or a list comment somewhere that Tailwind picks up.

Source Detection

Tailwind's default source detection scans the project while ignoring dependency and VCS directories. If you need explicit control, use @source and @source not directives in css/app.css:

@source "emails/**/*";
@source not "archive/**/*";

An explicit @source is only needed when default detection misses a source directory; an explicit @source not is useful for excluding source-like files that should not contribute utility classes.

What Lives in static/

Everything served directly to the browser sits in static/. The server's fetch fallback streams these files with aggressive caching in production (a one-year immutable cache header) and no-store in development.

A typical static/ layout:

static/
├── app.css                # compiled Tailwind (production)
├── app-dev.css            # compiled Tailwind (development)
├── alien-deepsignals.min.js # vendored reactive state library
├── form-controller.js     # live form validation
├── helpers-client.js      # $/$$ globals, navigate_to()
├── dialog-confirm.js      # confirm-button handler for <dialog>
├── checkbox-group.js      # bulk-select tables
├── file-upload.js         # file-input enhancement
├── image-upload.js        # image-input enhancement
├── web-components/        # custom elements
│   ├── validation-error.js
│   ├── toasts-area.js
│   ├── title-display.js
│   └── markdown-editor.js
├── favicon.svg
├── logo-dark.svg
└── logo-light.svg

The main browser-side patterns are documented in Client-Side UI Patterns.

What goes into app.css

Most styling happens directly in templates with Tailwind utilities - class="text-lg font-semibold mb-4" and so on. css/app.css is for project-wide concerns that don't belong inline:

  • Design tokens (@theme block) - see Theming & Tokens.
  • Element defaults (@layer base) - global resets, base typography, native control overrides.
  • Reusable patterns (@layer components) - class names you use across many templates (.nav-item, .button).
  • Custom utilities (@utility ...) - compositional shortcuts that compose with other Tailwind utilities (primary, secondary).
  • Cross-page CSS APIs - @view-transition, @page (print styles), @property (CSS custom properties with types).

If you're tempted to write a long sequence of utilities in a template and reuse it three times, that's a candidate for a @layer components class. If you're tempted to write the same component class twice, that's a candidate for one of the input components in components/.