Guide
CSS-Zero lets you write CSS styles in contract files (.css.ts/.css.js). At build time the compiler executes those files, replaces every utility call with a deterministic string token, and emits the matching CSS as a single chunk. Nothing runs in the browser.
CSS-Zero is inspired by vanilla-extract (type-safe .css.ts authoring with zero runtime) and Tailwind CSS (composable utility/variant classes), unified around a single idea: everything is a reusable token.
Why is it reuse-sharpened?
In my opinion, the value of code can often be measured by its reusability. CSS-Zero is built around making styles reusable without complexity or strict limitation.
The only requirement is to write styles in files with the .css.ts/.css.js extension. From there, reuse is a first-class concept at every level:
- Reusable rules —
className,classSelector,id,idSelector,variable,animation,font,layer,containerproduce small, self-contained tokens you can compose freely. - Reusable variants —
variantsandthemeturn a single base style into a family of modifiers, so one component can cover many visual states without duplicating CSS. - Reusable conditionals —
style(config, deps)lets you attach styles that are emitted only when the referenced tokens are actually used in the bundle. - Reusable libraries — a
.css.tsmodule is just a module. You can publish a styles library to NPM and consume it as a normal dependency in any project, importing tokens like any other export. The library can even be pre-built to.css.js— CSS-Zero understands both, so consumers never care how it was shipped.
Every utility returns a plain string or strings inside arrays / flat objects — nothing more. At build time the compiler substitutes each unique result with a special string called a token (e.g. o-s_1, --o-v_1) and emits the matching CSS. Because tokens are unique, unused styles are tree-shaken away — only the CSS you actually use ships, and none of this reuse costs anything at runtime.
As a result you define style once, reuse everywhere, and ship only what you use.
Install
npm i @css-zero/core @css-zero/vite-pluginSetup (Vite)
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { cssZero } from '@css-zero/vite-plugin';
export default defineConfig({
plugins: [react(), cssZero()],
});Write styles in a .css.ts file and import the tokens in your components.
// Button.tsx
import { btn } from './styles.css.ts';
export function Button() {
return <button className={btn}>Click</button>;
}Utilities
Individual rules
| Utility | Signature | Returns |
|---|---|---|
className | (rule?) => string | Unique class token (e.g. o-s_1) |
classSelector | (rule?) => [string, string] | [token, '.token'] |
id | (rule?) => string | Unique id token |
idSelector | (rule?) => [string, string] | [token, '#token'] |
variable | (config?) => [string, string] | [--token, 'var(--token)'] |
animation | (config?) => string | Unique @keyframes name token |
font | (config?) => string | Unique @font-face family token |
layer | () => string | Unique @layer token string |
container | (type?) => [string, string] | [container, '@container name'] |
Composed rules
| Utility | Signature | Returns |
|---|---|---|
variants | (config, base?) => Record<string, string> | Modifier token map, .base.mod selector |
theme | (vars, options) => [Record, Record] | [varRefs, optionClasses] |
style | (config, deps?) => string | Global styles (no deps) or conditional AND chunk |
Examples
className
import { className } from '@css-zero/core';
export const btn = className({
color: 'white',
'&:hover': { color: 'gray' },
});
// → 'o-s_1'classSelector
import { classSelector } from '@css-zero/core';
export const [card, cardSelector] = classSelector({
display: 'block',
padding: '16px',
borderRadius: '8px',
});
// card → 'o-s_1', cardSelector → '.o-s_1'id
import { id } from '@css-zero/core';
export const badge = id({ backgroundColor: 'red' });
// → 'o-i_1'idSelector
import { idSelector } from '@css-zero/core';
export const [modal, modalSelector] = idSelector({
position: 'fixed',
inset: 0,
});
// modal → 'o-i_1', modalSelector → '#o-i_1'variable
import { variable } from '@css-zero/core';
// [name, var(name)]
export const [accentKey, accent] = variable('#2b6cb0');
// → ['--o-v_1', 'var(--o-v_1)']animation
import { animation } from '@css-zero/core';
export const spin = animation({
from: { transform: 'rotate(0)' },
to: { transform: 'rotate(360deg)' },
});font
import { font } from '@css-zero/core';
export const inter = font({
src: "url('/fonts/inter.woff2') format('woff2')",
weight: '400 700',
display: 'swap',
});
// → 'o-f_1' — use it as `fontFamily: Inter`layer
import { layer } from '@css-zero/core';
export const base = layer();
// → '@layer o-l_1'container
import { container } from '@css-zero/core';
export const [card, cardQuery] = container('inline-size');
// card → 'o-c_1 / inline-size', cardQuery → '@container o-c_1'variants
import { variants } from '@css-zero/core';
export const tone = variants(
{ primary: { color: '#fff' }, ghost: { color: 'gray' } },
btn // optional base token → selector becomes `.btn.primary`
);
// → { primary: 'o-s_2', ghost: 'o-s_3' }theme
import { theme } from '@css-zero/core';
// [varRefs, optionClasses]
export const [tokens, options] = theme(
{ accent: '#2b6cb0', spacing: '8px' },
{ dark: { accent: '#0f0' }, compact: { spacing: '4px' } }
);
// tokens.accent → 'var(--o-v_1)'
// options.dark → 'o-s_1' (overrides accent when applied)style
import { style } from '@css-zero/core';
// no deps - always emitted
style({ body: { margin: 0 } });
// emitted only if all deps are used in the bundle
style({ [`${btnSelector} > span`]: { display: 'block' } }, [btnSelector]);Extending types
Merge your own custom CSS properties via interface declaration merging:
declare global {
namespace CSSZero {
interface ExtraProperties {
'--brand': string;
}
}
}Publishing a styles library
Contract modules are ordinary modules, so you can publish styles as an NPM package and consume them:
// your-lib/src/buttons.css.ts
export const [btn, btnSelector] = classSelector({ /* ... */ });
export const btnBg = variants({ primary: { /* ... */ }, ghost: { /* ... */ } }, btn);import { btn, btnBg } from 'your-lib/src/buttons.css';
<button className={`${btn} ${btnBg.primary}`}>Button</button>Plugin options
| Option | Type | Default | Description |
|---|---|---|---|
prefix | string | 'o' | Name prefix for every token (o-s_1, --o-v_1). |
build.inline | boolean | false | Inline CSS into a <style> tag in <head> instead of a <link> |
Important
Outside a bundler (SSR, Jest, tsc/IDE), @css-zero/core returns empty strings — the signatures are correct, the values are placeholders. The working path goes through @css-zero/compiler and @css-zero/vite-plugin.
Packages
| Package | Role |
|---|---|
@css-zero/core | Real utility types + string stubs for tsc/IDE |
@css-zero/compiler | Build-tool-agnostic compile engine |
@css-zero/vite-plugin | Vite facade: compiles, aggregates, inlines/emits CSS, strips imports |