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 it as a dev dependency. A Next.js project also adds @csszyx/unplugin directly, because its configs name that package (the Next.js steps below say where), and so does any project whose postcss.config lists @csszyx/unplugin/postcss; csszyx init does this for you, and adds these packages to dependencies. For a framework it does not detect, init installs Tailwind with @tailwindcss/postcss and prints a postcss.config.mjs that runs @csszyx/unplugin/postcss before it, plus the Vite lines for a project that builds with Vite. Where each package goes says which ones a production install can leave out.

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.

A production install only needs what the app loads while it runs. The engine, @csszyx/core, is a WebAssembly build plus one native binary for your platform, and only the build reads it. csszyx, @csszyx/unplugin and @csszyx/cli install it; @csszyx/runtime does not.

PackageWho loads it
@csszyx/runtimeYour app, while it runs: the build adds imports of it to your components
csszyx, @csszyx/unpluginThe bundler, while it builds
@csszyx/clicsszyx check, csszyx migrate, csszyx next prebuild
@csszyx/typesTypeScript
Terminal window
npm install @csszyx/runtime
npm install -D csszyx @csszyx/types

A browser build bundles everything it imports, so nothing csszyx installs is needed once it is built. With server-side rendering, Vite leaves dependencies external and the server loads them from node_modules while it runs, so keep @csszyx/runtime in dependencies and import szv, szcn and szr from it in code the server runs. On the server csszyx loads its Node entry, which loads the build plugin and the engine; in the browser it resolves to the same helpers @csszyx/runtime exports.

next start loads next.config when the server starts, so a package the config imports must be installed in production. The Turbopack setup imports @csszyx/unplugin/next. csszyx is only read while building, by the defineConfig import in csszyx.config.ts:

Terminal window
npm install @csszyx/runtime @csszyx/unplugin
npm install -D csszyx @csszyx/cli @csszyx/types

With output: 'standalone', the server reads a copy of the config written at build time and gets the files it needs from the build’s trace of node_modules, whichever list a package came from. A standalone build of an App Router app carries no csszyx package at all: the runtime is bundled into the server chunks. Every csszyx package can then be a devDependency:

Terminal window
npm install -D csszyx @csszyx/runtime @csszyx/unplugin @csszyx/cli @csszyx/types

A package that styles at run time with szr or @csszyx/dynamic, and runs no csszyx build, installs the runtime alone. The engine is an optional peer of @csszyx/compiler, which the runtime depends on, so it is not installed:

Terminal window
npm install @csszyx/runtime

transformSource and the other functions that call the engine live in @csszyx/compiler/engine. Install the engine beside the compiler, at the same version:

Terminal window
npm install @csszyx/compiler @csszyx/core
import { transformSource } from '@csszyx/compiler/engine';

Without @csszyx/core, the import fails with Cannot find package '@csszyx/core'. At another version, the first call stops with engine-version-skew.

The calls run on the native addon when its platform package is installed and on the WebAssembly build when it is not. Pass parser: 'wasm' to pin the WebAssembly build, as build.parser does for the plugin; CSSZYX_PARSER=wasm does the same for every call in the process, csszyx check included.

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 the theme groups refresh live, and the merge table csszyx next watch rewrites so szcn follows a stylesheet edit
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
    npm install @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 {
    // Your own turbopack config goes under `turbopack`; its `resolveAlias`
    // and rules are kept and csszyx's are merged in beside them.
    turbopack: csszyxTurbopack(),
    };

    To extend an existing config, call csszyxTurbopack({ turbopack: existingConfig }). Put loader options such as tailwindStylesheet beside turbopack. The helper rejects extra arguments and unrecognized top-level keys; rules, resolveAlias and other Turbopack settings belong inside turbopack.

  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-turbo-cache-unseeded) 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';

A prefix on the import line needs no csszyx setting:

@import "tailwindcss" prefix(tw);

csszyx reads it from the stylesheet and writes it before every class it emits, so sz={{ p: 4, hover: { bg: 'red-500' } }} becomes tw:p-4 tw:hover:bg-red-500. That holds on the bundler plugins (Vue and Svelte templates included), the Next.js Turbopack loader, csszyx next prebuild, the Jest transformer, csszyx check, and objects lowered at runtime in an app one of those builds. The Vue and Svelte adapters used on their own, without the csszyx bundler plugin, read no stylesheet: pass the prefix as their classPrefix option. The CDN build (csszyx/browser) does not read a prefix yet.

The build stops when the stylesheets it finds set different prefixes, or when a stylesheet that imports Tailwind does not compile, because either way some of the classes it emits would style nothing. When the project also holds stylesheets the app does not load (fixtures, old copies), list the real ones in tailwindStylesheet: --tailwind-stylesheet for csszyx next prebuild and csszyx next watch, and csszyxTurbopack({ tailwindStylesheet }) for the Turbopack loader.

On Next.js the other app’s directory can be left out instead: --ignore <patterns> on csszyx next prebuild and csszyx next watch leaves the stylesheets under it out of the prefix vote as well as its sources. The command writes the patterns into .csszyx/cache/stylesheet-facts.json and the Turbopack loader reads them from there, so next.config repeats nothing. A stylesheet under an ignored directory still votes when one of the scanned sources imports it: the import is evidence the app loads it.

important on the import line is not supported yet, and the build warns about it.

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
    npm install -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 lockfile npm wrote without the other platforms — npm 11.2.0 and earlier, the 10.x that Node 22 ships included, can drop the other platforms’ binaries from package-lock.json when it rewrites the lockfile while node_modules exists. A npm ci on another OS (say Linux CI, after a lockfile written on macOS) then installs no binary. npm 11.3.0 fixed it; pnpm 11 and yarn 4 do not do it.

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.

    To get the binary back, install without skipping optional packages, or, for the npm lockfile case, write the lockfile again with npm 11.3.0 or later:

    Terminal window
    npm install -g npm@latest
    rm -rf node_modules package-lock.json
    npm install

    0.17 could not fall back in these cases: its compiler also loaded a parser package with its own native binding, and that one was missing too.

  • 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.

engine-version-skew or runtime-version-skew stops the build — two csszyx packages in the install come from different releases. The engine must match @csszyx/compiler, and @csszyx/runtime must match @csszyx/unplugin, exactly; each message names both versions (see the compiler and the engine and the plugin and the runtime). List what is installed and what pulled it in, then install every csszyx package at one version:

Terminal window
npm ls @csszyx/core @csszyx/runtime
npm install csszyx@<version> @csszyx/runtime@<version>

Name every csszyx package your package.json lists, at the same <version>.

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 />