Styling
Global CSS, Tailwind v4, and where component-scoped styling fits in zfb.
Styling in zfb has two layers — global CSS and Tailwind v4 — and one well-supported pattern for everything else: utility classes on the markup itself.
Global CSS
The default template ships with styles/. This is plain CSS, processed by zfb’s CSS pipeline, and made available to every page. Use it for design tokens, resets, base typography, and anything else that should apply site-wide:
:root {
--color-text: #1a1a1a;
--color-bg: #ffffff;
--font-body: system-ui, sans-serif;
}
body {
color: var(--color-text);
background: var(--color-bg);
font-family: var(--font-body);
}
Imports inside CSS work as you would expect — split your styles across files and pull them together from global.css.
Tailwind v4
Tailwind v4 is opt-in via zfb.config.{ts,json}:
{
"tailwind": {
"enabled": true
}
}
When enabled, the zfb-css crate runs the bundled tailwindcss-v4 binary as part of the build. There is no per-project Tailwind install — you do not add tailwindcss to package.json and you do not maintain a tailwind.config.js. The compiler is built into zfb itself.
Once Tailwind is on, utility classes work in any .tsx file:
export default function Hero() {
return (
<section className="mx-auto max-w-2xl px-6 py-12">
<h1 className="text-3xl font-bold">Hello</h1>
</section>
);
}
Tailwind v4’s CSS-first configuration is supported through @theme directives in global.css — you customise tokens by editing CSS, not a JS config file.
Component-scoped styling
There are two well-supported patterns for component-level styling: Tailwind utility classes, and CSS Modules.
Tailwind utility classes
The simplest pattern is global CSS for site-wide concerns plus Tailwind utility classes for component-level styling. This keeps the build fast and the runtime trivial, and it maps cleanly onto Tailwind v4’s design-token model.
CSS Modules
For genuinely component-scoped CSS — class names that must not collide across components — zfb supports CSS Modules. Any file named *.module.css is a CSS Module: its class names are rewritten to scoped, file-stable identifiers at build time, so two components can both define a .button class without clashing.
Author the styles in a .module.css file:
.card {
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 1rem;
}
.title {
font-weight: 700;
}
Import the module with a default import and read class names off the imported object:
import styles from "./card.module.css";
export default function Card() {
return (
<div className={styles.card}>
<h3 className={styles.title}>Hello</h3>
</div>
);
}
At build time zfb resolves styles.card to the scoped class name (e.g. KdPA9G_card) — the rendered HTML carries that scoped class, and the scoped CSS is folded into the same hashed dist/ stylesheet as the rest of your CSS. There is no separate .css file per module and no runtime cost: the lookup is resolved during the build.
How it works:
import styles from ".must be a default import./ x. module. css" stylesis a plain object mapping your original class names to the scoped ones.- Access a class with member access —
styles.cardorstyles["card"]. Both work; computed access with a dynamic key does not, because the rewrite happens at build time. - Plain
.cssimports (a file not ending in.module.css) are still treated as global CSS — only the.module.csssuffix opts a file into scoping. - The
.module.cssfile is discovered when it is imported from a.tsx/.ts/.jsx/.jsfile underpages/,components/,layouts/, orcontent/.
Limitations:
- CSS Modules imported by bare specifier from
node_modules(e.g.import s from "@org/) are not scoped — only project-relativepkg/ x. module. css" .// .imports are.. / - The
:exportblock andcomposesdirective are not supported; use plain class selectors.
What lands in dist/
The build pipeline runs Tailwind and PostCSS, writes a hashed stylesheet to dist/assets/, and injects a <link rel="stylesheet"> into each rendered HTML page. The stylesheet reference is stable so CDN caches can hold it across deployments until the content changes.