Styling
Global CSS, zudo-wind utilities, and component-scoped styling in zfb.
Style a zfb project with authored global CSS, utilities generated by zudo-wind, and CSS Modules when a component needs scoped selectors.
For utility styling, browse the utility reference by task. The overview explains how candidates resolve, and configuration covers the tokens and resets your project supplies.
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 project properties, resets, base typography, and anything else that should apply site-wide.
zfb resolves the global stylesheet from either styles/ or src/ — the top-level styles/ wins if both exist. The src/styles/ fallback exists for projects that organise sources under src/ (a common Vite/Astro/Next-style layout).
: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.
Package CSS imports such as @import "@scope/ resolve through the nearest installed package's package.json exports map. For conditional exports, zfb checks the style and default conditions in the order they appear in that package's map. A subpath that exports does not expose, or maps to null, cannot be reached through its physical file path; this follows Node's package-encapsulation rule. Packages without exports resolve subpaths by their physical paths. For a bare import such as @import "pkg", zfb checks the package's style field, then exports["."], then a CSS main.
zudo-wind
zudo-wind is the utility CSS language built into zfb. Utilities use the wind configuration key; an omitted key behaves like {}: generation is enabled, the reset is none, and no tokens or breakpoints are supplied implicitly. Configure only the values your project uses. The zudo-wind overview and its configuration, source, and utility guides cover the language.
Built into zfb
The compiler is part of the zfb binary. It does not launch a separate CSS-engine process, look for a binary override, or require a project install. Authored CSS and CSS Modules remain available alongside generated utilities.
Source discovery
Build and dev read complete candidate strings from the configured source plan in .tsx, .ts, .jsx, .js, .mdx, and .md files. The plan can include conventional project roots, declared package roots, mirror roots, and explicit sources. It reads source text rather than running the application; see Sources and candidates for how candidates are extracted and extended.
The standalone zfb css --source command also accepts explicit .html and .mjs sources; build and dev do not discover those extensions automatically. Its flags and authored-CSS behavior are covered in the CLI reference.
Authored CSS only
Set wind: false to disable wind generation, token validation, manifest loading, and wind candidate scanning:
{
"wind": false
}Global CSS, CSS Modules, and syntax highlighting continue to run. Checks for leftover wind directives in stylesheets still run as well. A non-none reset can emit CSS without utility candidates; an empty enabled configuration emits no wind stylesheet when it has no resolved rules or tokens. See Cascade and reset for the output order.
Component-scoped styling
Use zudo-wind utilities for compact component styling and CSS Modules when selectors need component-local names.
zudo-wind utility classes
The source files hold complete class candidates; configured tokens and breakpoints determine which candidates resolve to CSS. For example, static layout utilities can appear directly in JSX:
export default function Toolbar() {
return (
<nav class="flex items-center justify-between">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
);
}See the utility reference for supported classes and tokens for project-defined values.
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 class={styles.card}>
<h3 class={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 from JavaScript/TypeScript modules are not emitted. zfb warns when the bundler resolves one;strictPlainCssImports: trueturns the warning into a build error. The default isfalse;zfb build --strict-plain-css-imports/--no-strict-plain-css-importsoverride the config for one build, and the paired flags conflict if both are passed. This gate affectszfb buildonly;zfb devremains warning-only. Put global CSS instyles/(and useglobal. css @importthere), or use a default-imported.module.cssfile for scoped component CSS. See the CLI reference for the other build flags.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.
Monorepo / workspace-sibling packages
In a pnpm-workspace-style monorepo, a sibling package reached only through a tsconfig path alias (not a plain relative import) is still picked up by the CSS pipeline:
The sibling's own
*.module.cssfiles join the same CSS Modules scan as project-local files, so their scoped class names resolve the same way as any other module.The sibling's source files also feed zudo-wind's source plan, so candidates used only inside the sibling package can be generated too — not silently dropped.
The aliased target becomes a "mirror root": the CSS pipeline treats a claimed workspace-sibling directory the same way it treats project source roots. See Sources and candidates for the source-plan model.
What lands in dist/
The build pipeline combines authored CSS, any generated wind output, and CSS Modules output into a hashed stylesheet under dist/assets/, then 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.