Skip to content

Tailwind build

Danbyte ships its CSS as a precompiled static stylesheet rather than running the Tailwind Play CDN's JIT compiler in the browser. The CDN works fine for a static mockup, but in an htmx app it re-scans the DOM on every swap and shows up as Interaction-to-Next-Paint regressions (we measured 160–250 ms INP on the space map before this).

Files

File Role
package.json Pins tailwindcss@^3.4 as a devDependency + defines the build:css script.
tailwind.config.js content: paths the compiler scans for class usage. Includes Python files that emit class strings (widgets, forms, template tags) so utility classes generated from server code aren't purged.
design/tailwind.input.css Entry — @tailwind base; components; utilities;
design/tailwind.css Compiled output. Served as a static file from /static/tailwind.css. Committed to the repo so a fresh clone runs without Node.
design/tokens.css Hand-written tokens loaded after the Tailwind stylesheet — .ck checkbox, .space-cell*, etc. Kept out of the Tailwind compile so we can tune values without a rebuild.

Building

make css-install     # first time only — installs tailwindcss into node_modules/
make css             # one-shot build (production, minified)
make css-watch       # rebuild on save while editing templates

make css is what CI / deploys run. Local development can use make css-watch in a side terminal.

Adding new classes

The compiler only ships classes it finds in the content: glob. If you add a class via a path the config doesn't scan (e.g. a new Python module that injects HTML), do one of:

  • Add the path to content: in tailwind.config.js, OR
  • Add the bare class to design/tailwind.input.css so it always ships, OR
  • Use safelist in the config if the class name is computed at runtime.

When in doubt, add the path — purging is cheap, debugging "this class didn't show up" is not.

Why not the Play CDN

The Play CDN ships ~150 KB of JavaScript that does these things:

  1. MutationObserver on document.body — fires on every htmx swap.
  2. Class extraction — re-scans every node looking for class="...".
  3. CSS generation + injection — recomputes utility CSS and re-injects it as a <style> tag.

All three run on the main thread, and (1) fires on every single htmx swap in this app: tab changes, filter rail changes, picker re-renders. The space map at /prefixes/_map_picker/ has 500+ cells; every change of the parent prefix dropdown triggered a full JIT recompile that pegged the main thread for 100–250 ms.

The static build eliminates all three steps. The browser sees one <link rel="stylesheet" href="/static/tailwind.css"> request the first time, caches it, and never thinks about Tailwind again.

Production note

The Django dev server doesn't gzip static files. Behind nginx (or via whitenoise's CompressedManifestStaticFilesStorage), the 38 KB stylesheet becomes a ~9 KB transfer.