Skip to content

Installation

Terminal window
npm install csszyx

The csszyx umbrella package includes the compiler, runtime, types, and build plugin. The command-line tools (migrate, next, type generation) ship separately as @csszyx/cli — run them with npx @csszyx/cli <command> or install with pnpm add -D @csszyx/cli. A Next.js project also adds @csszyx/unplugin directly, because its configs name that package (the Next.js steps below say where); csszyx init does this for you.

For portable sz autocomplete through compatible TypeScript editor hosts, install the preview @csszyx/ts-plugin separately and follow the TypeScript Autocomplete Plugin guide. It is an authoring tool, not a build dependency.

vite.config.ts
import { defineConfig } from 'vite';
import csszyx from 'csszyx/vite';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
// Order matters: csszyx → tailwindcss → react
...csszyx(),
tailwindcss(),
react(),
],
});

What the Turbopack lane can and cannot use

Section titled “What the Turbopack lane can and cannot use”

csszyx’s Turbopack support is a webpack-compatible loader wired through turbopack.rules, so it is bounded by how much of the webpack loader API Turbopack implements. This is the state csszyx builds on, taken from the Next.js 16 documentation and validated against Next.js 16.2.x, the version the csszyx playground pins and CI runs.

CapabilityStatusWhat csszyx does with it
turbopack config keyNamed turbopack from 15.3; was experimental.turbo in 13.0.0–15.2.x, removed in 16csszyxTurbopack() writes this block
turbopack.rules loadersSupportedRuns the sz transform
File dependencies (this.addDependency)Supported — Turbopack reports a loader’s file dependencies back to its watcherWatches your @theme stylesheets so szcn groups refresh live
resolveAliasSupportedNot used — the theme registration imports a real relative path, so no app config is needed
this.emitFileNot supportedWhy the generated registration is written to .csszyx/ directly instead of emitted
this.modeNot supportedcsszyx falls back to NODE_ENV, so dev/production detection is unaffected
this.importModule / this.loadModuleNot supportedUnused
this.fsPartial — readFile onlyUnused
Deep bundle rewrite hookNot exposedWhy production mangling stays Webpack-only
  1. Add @csszyx/runtime and @csszyx/unplugin as direct dependencies. The transform injects a bare import { _szMerge } from '@csszyx/runtime', and the two config files below name @csszyx/unplugin/next and @csszyx/unplugin/postcss by package; a strict package manager (pnpm, Yarn PnP) does not resolve either as a transitive dependency of csszyx. csszyx init installs both.

    Terminal window
    pnpm add @csszyx/runtime @csszyx/unplugin
  2. Wire the loader with the csszyxTurbopack() helper. It sets the *.tsx loader rule correctly (without as, which would otherwise self-match into ./X.tsx.tsx) and merges your existing turbopack config.

    next.config.mjs
    import { csszyxTurbopack } from '@csszyx/unplugin/next';
    export default {
    turbopack: csszyxTurbopack(
    {}, // your existing turbopack config (resolveAlias, other rules) is merged
    ),
    };
  3. Let PostCSS point Tailwind at the safelist. Turbopack gives csszyx no hook into your stylesheet, so the @source directive the other bundlers inject for you comes from a PostCSS plugin here. List it before @tailwindcss/postcss — Tailwind compiles the stylesheet in its own pass, so a plugin after it is too late. csszyx init writes this file for you.

    postcss.config.mjs
    export default {
    plugins: {
    '@csszyx/unplugin/postcss': {},
    '@tailwindcss/postcss': {},
    },
    };

    The plugin names the safelist csszyx writes by default, .csszyx/csszyx-classes.txt, relative to each stylesheet that imports tailwindcss, and skips a path you already wrote yourself. If you point csszyx next prebuild, csszyx next watch or the loader’s safelistOutputFile somewhere else, list every file it should name: '@csszyx/unplugin/postcss': { safelistFiles: ['.csszyx/my-classes.txt'] }. A safelist that does not exist yet is fine: Tailwind scans nothing for it and does not fail.

  4. Keep the Tailwind safelist fresh. Run the csszyx watcher/prebuild around Next. Production builds fail-closed (Next Turbopack production cache is not ready) until csszyx next prebuild has seeded the safelist and generation manifest, so wire both flows into your scripts:

    package.json
    {
    "scripts": {
    "dev": "concurrently \"csszyx next watch 'app/**/*.tsx'\" \"next dev --turbopack\"",
    "build": "csszyx next prebuild 'app/**/*.tsx' && next build --turbopack"
    }
    }
    • dev: csszyx next watch maintains the safelist while next dev runs (the example uses concurrently; a second terminal works just as well)
    • build: csszyx next prebuild must finish before next build starts — running it as part of the build script keeps plain pnpm build working

CSSzyx requires Tailwind CSS v4. Create a CSS entry point:

src/index.css
@import "tailwindcss";

Import this in your app entry point:

src/main.tsx
import './index.css';
csszyx({
development: {
debug: true, // Enable debug logging
},
production: {
mangle: true, // Obfuscate class names (z, y, x, ...) — opt-in, off by default
},
build: {
astBudgetLimit: 50_000, // Per-file AST node cap; file skipped (warned) past it
scanCss: 'src/index.css', // CSS file(s) to scan for @theme tokens
},
});

For per-element hydration recovery (szRecover="csr" / szRecover="dev-only") see SSR & Hydration → Recovery Tokens.

For SSR hydration safety, initialize the runtime in your app entry:

import { initRuntime } from '@csszyx/runtime';
initRuntime({
development: process.env.NODE_ENV === 'development',
strictHydration: true,
});

This is optional — CSSzyx works without it, but SSR hydration guards require it to be called before first render. Per-element CSR recovery is opted in via the szRecover JSX attribute on individual elements; see SSR & Hydration.

The sz prop comes from a JSX type augmentation in @csszyx/types. It is picked up automatically when you import from csszyx in a hoisting package manager. With a strict package manager (pnpm), or if your app uses sz without importing csszyx directly, add a one-line reference so TypeScript loads the augmentation.

  1. Install the types so the reference resolves at the top level:

    Terminal window
    pnpm add -D @csszyx/types
  2. Add a csszyx-env.d.ts at your project root (kept in your tsconfig.json include):

    /// <reference types="@csszyx/types/jsx" />

    SolidJS keeps its own JSX namespace, so reference the Solid augmentation instead:

    /// <reference types="@csszyx/types/jsx-solid" />

    Solid consumers must add @csszyx/types as a direct dev dependency (pnpm add -D @csszyx/types) — unlike the React augmentation, the Solid one is not picked up transitively through csszyx, so without the direct dependency astro check / tsc report Property 'sz' does not exist on Solid elements.

Then the sz prop is typed on every element:

// ✅ TypeScript knows this is valid
<div sz={{ p: 4, bg: 'blue-500', hover: { bg: 'blue-700' } }} />
// ❌ TypeScript error: 'red-999' is not a valid color
<div sz={{ bg: 'red-999' }} />

Classes not applying — make sure your CSS entry point uses the full Tailwind v4 bundle:

/* ✅ correct — includes theme + preflight + utilities */
@import "tailwindcss";
/* ❌ wrong — utilities only, theme variables undefined */
@import "tailwindcss/utilities";

Partial imports like tailwindcss/utilities only generate static-value utilities (.border-0, .m-px). Scale-dependent utilities like p-4, rounded-sm, text-xs require the theme layer (--spacing, --radius-sm, --text-xs) and will silently produce no CSS without it.

Monorepo with both Tailwind v3 and v4 — if your workspace has other packages that depend on Tailwind v3, your package’s CSS resolver may accidentally pick up v3 instead of v4, causing @import "tailwindcss" to fail or generate no theme utilities.

Fix: explicitly declare tailwindcss as a dependency in each package that uses CSSzyx:

{
"dependencies": {
"csszyx": "^0.4.0",
"tailwindcss": "^4.0.0"
}
}

Plugin order warnings — CSSzyx must be before Tailwind and React in the plugins array.

“No prebuilt native binary” warning, or a native engine unavailable build error — CSSzyx parses your source with one engine that ships as two artifacts. The default is rust (a native addon) because it is the fastest. The same engine is also compiled to WebAssembly and ships inside @csszyx/core itself, so a machine with no native binary still runs the engine every other machine runs — identical output, only parse speed differs.

Because both artifacts are the same engine, neither can change your classes. There is nothing else to fall back to and nothing else to choose.

The native engine ships as per-platform optional dependencies (@csszyx/core-<platform>) — one is auto-selected for your OS/CPU/libc during a normal npm install. You do not run any extra command or compile anything; it is just a prebuilt download. It can be absent in three situations:

  • Unsupported platform — your OS/arch/libc is outside the prebuilt set (the targets are macOS arm64/x64, Linux gnu+musl arm64/x64, Windows arm64/x64). Anything else (FreeBSD, 32-bit ARM, riscv64, Alpine on an odd arch, …) has no matching package.
  • Optional dependencies were skipped — installing with --omit=optional / --no-optional, or in a minimal/sandboxed CI or Docker image that strips optional deps, removes the binary even on a supported platform.
  • A cross-platform lockfile — a lockfile generated on one OS (say macOS) then installed on another (say Linux CI) with --frozen-lockfile may not have the other platform’s optional entry resolved, so the binary is missing.

What happens, and what to do:

  • You only inherited the default rust (you did not set build.parser or the CSSZYX_PARSER env var): CSSzyx automatically falls back to the engine’s wasm build and prints the warning once. Your build succeeds with identical classes; only parse speed differs. To silence it, pin the wasm build (next bullet) or install the matching @csszyx/core-<platform> package / stop omitting optional deps.

  • You explicitly chose rust (via config or env): this stays a hard error on purpose — an explicit choice is never silently swapped. Either install the native binary for your platform, or ask for the wasm build by name:

    // vite.config.ts / csszyx plugin options
    csszyx({ build: { parser: 'wasm' } })
    Terminal window
    # one-off, e.g. in CI
    CSSZYX_PARSER=wasm npm run build

'rust' and 'wasm' are the only values. They are the same engine, so the choice is about how it loads, never about what it emits.

TypeScript errors with sz prop (Property 'sz' does not exist on type 'DetailedHTMLProps<...>') — the JSX augmentation is not loaded. Add @csszyx/types and a csszyx-env.d.ts with /// <reference types="@csszyx/types/jsx" />; see TypeScript above. This is common under pnpm and when csszyx is never imported directly.

Hydration warnings in Next.js — initialize the runtime in your root layout and opt in to per-element recovery with <section szRecover="csr">…</section> where mismatches are expected. See SSR & Hydration.

sz props not working in Astro MDX — the MDX Vite plugin compiles JSX to runtime calls before csszyx runs, so sz={{ ... }} props written directly in .mdx files are never transformed and render as sz="[object Object]".

Fix: put sz props in .tsx components and import them in MDX:

// src/components/MyDemo.tsx ← compiled by csszyx ✅
export function MyDemo() {
return <div sz={{ p: 4, bg: 'blue-500' }}>Hello</div>;
}
{/* src/pages/guide.mdx */}
import { MyDemo } from '../../components/MyDemo.tsx';
<MyDemo />