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:intailwind.config.js, OR - Add the bare class to
design/tailwind.input.cssso it always ships, OR - Use
safelistin 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:
- MutationObserver on
document.body— fires on every htmx swap. - Class extraction — re-scans every node looking for
class="...". - 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.