Skip to content

Warnings & Troubleshooting

Every message csszyx prints is listed here with its exact wording, so you can paste a line from your terminal or console into Ctrl+F and land on the row that explains it.

Placeholders are written the way the source writes them — {filename}, {key}, {n} — and are filled in at emit time.

Every message starts with [csszyx]. Two things decide where it shows up and whether you can ignore it:

  • Where it runs. Build messages come from the bundler plugin, the compiler, or the CLI, and appear in the terminal. Runtime messages come from the runtime helpers and appear in the browser console (or the SSR terminal).
  • What it costs. Some messages report that CSS is missing from the output. Others are usage advice — the styles are fine, the code could be better. csszyx treats those two very differently.

The most common build message has a fixed shape:

[csszyx] /src/components/Card.tsx
sz fallback at 12:9: identifier `cardStyles` could not be resolved to a static value.
Suggestion: Make sure it's a module-level or function-body const with a literal object value. For variant-based styling → szv(). For true runtime values → dynamic().
PartMeaning
sz fallbackThe site. One of sz fallback, szr fallback, szv catalog.
12:9Line and column of the expression, 1-based.
reasonWhat could not be read — the expression shape.
Suggestion:The way out, chosen by the expression shape.

This is the single most useful distinction in this page.

ClassMeansDev buildProduction buildquiet: 'nudges'quiet: true
missing outputClasses never reached the safelist, so no build step was told to generate their CSS. The markup names utilities that do not exist.printsprintsprintssilent
usage nudgeThe classes were collected. The message is advice for whoever writes csszyx code.printssilentsilentsilent

A separate group of build messages goes straight to console.warn and is not gated by quiet at all — parser fallbacks, prescan skips, mangle reports, and the native-binary notice. Each is marked in its table below.

SwitchSilencesWhere
quiet: 'nudges'Usage nudges, keeps missing-output reportsPlugin option
quiet: trueAll plugin warnings (errors still throw)Plugin option
contentScopeCheck: falseThe unscoped-monorepo warning onlyPlugin option
CSSZYX_QUIET_SZ_WARNINGS=1The sz key/value dev warnings (§ sz key and value warnings)Environment
CSSZYX_NO_PROJECT_SCAN_HINT=1The one-time csszyx check tipEnvironment
csszyx.enableDiagnostics: falseVS Code squigglesEditor setting
NODE_ENV=productionEvery runtime dev warning (they are dead-code-eliminated)Environment

See Plugin Config → Quiet for the option itself.

What you actually seeStart here
An element has no styling at allsz fallback, safelist and CSS entry
One property does nothing, the rest workssz key and value warnings
sz="[object Object]" in the DOMszr and the string helpers
class="[object Object]"szv runtime
Styles work in dev, break in productionMangling, SSR and hydration
A variant (hover:, md:) never appliessz key and value warnings
Slots on a component are unstyledszs and szsc
Merging two class strings keeps bothszcn
The build got slower or biggerMangling
Only some files are styledBuild pipeline

These fire when the compiler cannot read the value you put in sz={…}. All both engine artifacts emit identical wording.

What each kind means:

KindTriggered byClasses still collected?Fires in production?
callsz={makeSz()}Yes — dynamic() and friends compile correctlyNo (nudge)
identifiersz={styles} where styles is not a readable constYes, when a caller supplies itNo (nudge)
importsz={imported} from another moduleNoYes
membersz={theme.card}Yes, when a caller supplies itNo (nudge)
szv-factorysz={myVariants({ … })} where the compiler saw myVariants = szv(…) and refused its configYes — the runtime szv path rendersNo (nudge)
otheranything else — a template literal, a conditionalYesNo (nudge)

Exact wording{detail} is the callee name, identifier name, or node type:

KindReasonSuggestion
callfunction call `{detail}()` result is unknown at build timeIf it returns static variants → convert to szv(). If it depends on runtime data → use dynamic().
identifieridentifier `{detail}` could not be resolved to a static valueMake sure it's a module-level or function-body const with a literal object value. For variant-based styling → szv(). For true runtime values → dynamic().
importimported binding `{detail}` could not be read at build timeExport it as a const with a static object literal — a star re-export or a computed value keeps the runtime path, as does build.importedStaticSz: false. For variant-based styling → szv(). For true runtime values → dynamic().
membermember expression is not statically resolvableExtract the value to a module-level const. For variant-based styling → szv(). For true runtime values → dynamic().
szv-factoryszv factory `{detail}()` did not precompile — its config disqualified at `{path}` Every variant value must be a static sz object literal with canonical keys and non-overlapping branches. Fix the value at that path. For runtime data → dynamic().
otherexpression of type `{detail}` is not statically analyzableUse a literal sz object or a module-level const. For variant-based styling → szv(). For true runtime values → dynamic().

When the callee has no readable name — sz={obj[key]()}{detail} is ?.

The szv catalog site replaces the suggestion with one of its own:

Pass the config inline, or as a module-level const object literal. A computed or spread config cannot be read at build time, so none of its variant classes are safelisted and they generate no CSS.
MessageClassFix
[csszyx] unresolvable sz spread at {line}:{col}: sz={{ ...x }} cannot be resolved at build time and falls back to runtime; it may render no styles in production. Use array form: sz={[x, { ... }]}.missing outputUse the array form.
sz array element at {line}:{col}: this object literal contains a runtime value, so the whole element is deferred to _szPart at runtime (its classes are still safelisted best-effort). — suggestion: use finite literal ternary branches when possible, or move truly runtime values to dynamic().nudgeOptional. Prefer literal ternary branches.
[csszyx] possible style override at {filename}: this element spreads props that may contain style, while sz emits an explicit style attribute. Move the spread style to an explicit style prop so csszyx can merge both values.nudgePass style explicitly so both values merge.
[csszyx] szRecover at {filename}: only string-literal values ("csr" | "dev-only") are supported. Dynamic values disable token emission for this element.nudgeUse a literal.
[csszyx] szRecover at {filename}: unknown mode "{value}" — expected "csr" or "dev-only". Token emission skipped.nudgeUse "csr" or "dev-only".
[csszyx] "sz" takes precedence over the runtime "className" on this element at {filename}:{line}, whatever order the attributes are written. If the className carries overrides from a caller, they are dropped. — suggestion: state the order in one sz array — sz={[{ … }, className]} for the caller to win, sz={[className, { … }]} for these styles to win.nudgeState the order in one sz array. Either rewrite silences it.

{filename} is <anonymous> when the compiler was called without one.

Fallbacks a production build does not list

Section titled “Fallbacks a production build does not list”

A fallback at an sz prop is advisory: the runtime path works and the classes are still collected, so a production build does not print it. Only the kinds that mean absent outputszr, szs, spreads, budget bails — print in every mode.

That makes the fallbacks a production log does list a lower bound, never a total. So the build closes with a count of what it held back:

[csszyx] {n} advisory sz fallbacks not listed above. At an sz prop a fallback is advisory — the runtime path works and the classes are collected — so a production build keeps the list short. A development build prints each one with its file and position.

Run a development build to see each one with its file and position. Silenced by quiet: true (blunt mode) along with everything else; quiet: 'nudges' keeps it, because one line saying how much was left out is what stops a calm log from reading as a clean one.

These come from the shared lowering core, so the same message appears whether the value was lowered at build time or by the runtime.

{ at LOC} below is an optional at src/Card.tsx:12 suffix — present when a build engine knew the location, absent on the runtime path.

MessageNotes
[csszyx] Unknown property "{key}" in sz prop{ at LOC}. The class is still emitted, so it styles nothing unless Tailwind serves that utility. Check for typos. If the class is intentional, define it with Tailwind's @utility.The class is in the DOM — grep for it.
[csszyx] "{key}" was removed{ at LOC}: {note}.The removed key emits no class. Apply the replacement shape from the note.
[csszyx] Use the canonical key "{suggestion}" instead of "{key}"{ at LOC}.The alias emits no class; rename it to the canonical key.
[csszyx] sz received a numeric key "{key}"{ at LOC}. This usually means an array or a spread was passed where an object of sz keys was expected. The value is ignored.You probably passed an array to sz={{ … }}.
[csszyx] "{rawKey}" boolean sugar was removed{ at LOC}. Use { {key}: '{value}' } instead, or run ``csszyx migrate``.browser too. The build lane reports it as well, with the position — a statically extracted prop never reaches the runtime, so that is the only channel that can tell you.

When a warning has no source location, csszyx appends a traceability tail so you can find it:

sz object was {"p":4,"bgg":"red-500"} · from at Card (/src/Card.tsx:8:5)

And once per process, a pointer at the whole-project scan:

[csszyx] Tip: run `npx @csszyx/cli check` to scan every file for sz key issues at once (dev warnings only surface files as you open them).

The parenthetical is this warning has no source location — the scan reports which file and key triggered it when the warning was unlocated.

Unknown custom keys and removed keys deliberately have different outcomes. A brand-new key can still target a project-defined Tailwind @utility, so csszyx emits its kebab-case class after warning. A key listed as an alias or removed migration is not a custom escape hatch: csszyx warns and drops it instead of silently preserving obsolete API spellings or leaving dead classes in the DOM.

MessageNotes
[csszyx] "{key}: {value}"{ at LOC}: {value} is not on Tailwind's spacing scale (quarter steps only), so the class generates no CSS. Use a quarter step (1.25, 1.5, 1.75) or a unit value ("{value}rem").
[csszyx] "{key}" is a property, not a variant, but received an object { … }{ at LOC}. This compiles to "{key}:*" classes that match no Tailwind variant and generate no CSS. Move the nested keys up a level, or for color opacity use { color: '...', op: ... }.The classic p: { bg: … } mistake.
[csszyx] {rawKey}: '{value}' is a CSS value — use the short form '{hint}' (e.g. { {rawKey}: '{hint}' }). '{rawKey}-{value}' has no Tailwind utility and renders nothing.browser too. flex-start → start, space-between → between, …
[csszyx] "{className}"{ at LOC}: the /{opacity} opacity modifier will not apply — the "{color}" theme token resolves to the bare comma triplet "{value}", which color-mix() cannot dim. Wrap the variable, e.g. --color-{color}: rgb(var(--your-triplet)).browser only — it needs computed style to resolve the token. csszyx check answers the same question from the compiled stylesheet.
[csszyx] fontSmoothing: '{value}' is not supported — use 'grayscale' or 'subpixel'.
[csszyx] fontStyle: '{value}' is not supported — Tailwind only models 'italic' and 'normal'. For oblique, use css: { fontStyle: '{value}' }.
[csszyx] "{key}" cannot take a runtime value at {LOC}: Tailwind has no utility for this key that reads a CSS variable, so the class and the variable were dropped instead of styling a different property. Name the values with szv(), or use dynamic() for open-ended data.Build only — a literal on the same key is unaffected. See below.
[csszyx] "{key}: '{value}'"{ at LOC}: Tailwind has no per-side border style, so this generated no CSS and the class is dropped. Use borderStyle: '{value}' for every side, or a number on "{key}" for the width.A style keyword on a side key. See below.

Border styles are set for every side, not per side

Section titled “Border styles are set for every side, not per side”

CSS gives each side its own border-style; Tailwind spells the style at the root only. borderStyle is the key for it, and the side keys take a width:

<div sz={{ borderStyle: 'none' }} /> // ✅ border-none — every side
<div sz={{ borderB: 2 }} /> // ✅ border-b-2 — this side's width
<div sz={{ borderB: 0 }} /> // ✅ border-b-0 — this side, no border
<div sz={{ borderB: 'none' }} /> // ❌ dropped: border-b-none has no rule

border-b-none reached the safelist, Tailwind generated nothing for it, and the border stayed. All six style keywords behave this way on all ten side keys — the four physical sides, the two axes, and the four logical ones. Widths, colours and theme tokens are unaffected on every one of them.

A runtime value normally compiles to <prefix>-(--_sz-<key>) plus a style custom property. That works whenever the var form is the same utility as the literal form with the value deferred — which is most of the sz vocabulary, but not all of it.

<div sz={{ color: tone }} /> // ✅ text-(--_sz-color) — a colour utility
<div sz={{ textAlign: align }} /> // ❌ dropped: text-(--v) is a COLOUR, not an alignment
<div sz={{ display: how }} /> // ❌ dropped: display-(--v) is not a utility at all

color and textAlign share the text- prefix and only one of them has a var form, so this is per key, never per prefix. Both failures used to be silent: the first wrote color: var(…) holding something like "center", an invalid value that also unsets the inherited colour; the second put a class in the safelist that Tailwind generated no rule for.

<div sz={{ p: 4, textAlign }} />
// → <div className="p-4" />
[csszyx] "{key}" cannot take a runtime value at {LOC}: Tailwind has no utility for this key that reads a CSS variable, so the class and the variable were dropped instead of styling a different property. Name the values with szv(), or use dynamic() for open-ended data.

The sibling keys survive — only the one property is dropped, and the style attribute is not emitted at all.

One or two options — a ternary between literals. Nothing else needed; this is fully build-time.

<div sz={{ p: 4, textAlign: centered ? 'center' : 'left' }} />
// → className={`p-4 ${centered ? "text-center" : "text-left"}`}

A real set of options — name them with szv. Read the factory through szr on className: that is the position the compiler resolves at build time.

const alignSz = szv({
base: { p: 4 },
variants: {
align: {
left: { textAlign: 'left' },
center: { textAlign: 'center' },
right: { textAlign: 'right' },
},
},
});
<div className={szr(alignSz({ align: side }))} />
// → className="p-4 text-left" | "p-4 text-center" | "p-4 text-right"
// all three safelisted, no runtime helper

An open-ended value from data — dynamic(). Only when the value genuinely is not one of a known set; it injects CSS in the browser.

Every key below takes a literal, a ternary between literals, or an szv variant — but not a bare runtime value. You do not have to memorise it: the build names the key and the line when you hit one.

Reference pageKeys
BackgroundsbgAttach bgClip bgImg bgOrigin bgRepeat bgSize
BordersborderStyle outlineStyle
EffectsmaskClip maskComposite maskConic maskLinear maskMode maskOrigin maskRepeat maskType mixBlend
Flex & GridalignContent flexDir flexWrap gridFlow items justify justifyItems justifySelf placeContent placeItems placeSelf self
Interactivityappearance fieldSizing pointerEvents resize scheme scroll scrollbar scrollbarGutter select snapAlign snapStop snapType touch
Layoutbox boxDecoration breakAfter breakBefore breakInside clear display float isolation notSrOnly objectFit overflow overflowX overflowY overscroll overscrollX overscrollY position srOnly visibility
MiscborderCollapse caption forcedColorAdjust tableLayout
Sizingcontainer
Transformsbackface transformStyle
TransitionstransitionBehavior
Typographydecoration decorationStyle fontFamily fontSmoothing fontStyle fontVariant listPos ordinal slashedZero text textAlign textClip textEllipsis textTransform textWrap whitespace

Membership is derived, not decided: pnpm check:var-hostile-keys compiles both forms of every documented key through the pinned Tailwind and compares which CSS properties each one sets. A Tailwind version that adds an arbitrary-value form for one of these keys fails that gate until the key is taken off the list.

Two things this never touched: a static value on these keys, which has always lowered to the keyword utility, and the runtime_sz({ textAlign: side }) receives a real value and has always been correct.

Both messages exist on two paths with identical text — the value is dropped either way.

Message
[csszyx] "{key}: '{value}'" is not a recognized color value and will be ignored. Use a Tailwind color ("blue-500"), CSS variable ("--my-color"), hex/rgb/hsl ("#ff0000"), or object form ({ color: "blue-500", op: 50 }).
[csszyx] "{key}: '{value}'" — string slash opacity is not supported. Use object form: { color: '{color}', op: {opacity} }.
MessageNotes
[csszyx] {owner}: unknown field "{member}"{ at LOC} — nothing is emitted for it. {owner} takes { {allowed} }.browser too. Catches maskLinear: { form: … } for from.
[csszyx] mask: '{value}'{ at LOC} — gradient layers moved to "{key}". Tailwind composites mask-image from one variable per layer, so each layer is its own key. `mask` now takes a direct mask-image only: none, a url(), or a CSS variable.browser too. See Effects & Filters.

An szv() config the compiler cannot read is reported as szv catalog at … using the fallback matrix reasons and the szv-specific suggestion. It is always missing output: an unreadable config means the variant catalogue is never extracted, so none of its classes are safelisted.

All of these are dev-only, deduplicated, and removed entirely from a production bundle. The [csszyx] prefix is added by the shared helper.

Message
szv(config): config must be an object, got {value}. Ignoring.
szv(config): base must be an sz object, got {value}.
szv(config): variants must be an object when present, got {value}. Ignoring.
szv(config): defaultVariants must be an object, got {value}.
szv(config): variants.{dimension} must be an object of values, got {value}.
szv(config): variants.{dimension}.{token} must be an sz object, got {value}. It will be skipped.
szv(config): {where} nests deeper than {max} levels; it will be rejected at render.
szv(config): {where} has a forbidden key "{key}"; it will be skipped.
szv()(selection): unknown variant "{key}" — not declared in config.variants.
szv()(selection): "{value}" is not a value of variant "{key}" — it has no styles.

The last two also fire from the precompiled fast path, with identical wording.

If you see [object Object] in class:

[csszyx] szv() returned an sz OBJECT that was used as a string (e.g. className={someSzv({...})}) — this renders "[object Object]". Pass it to an sz= prop, or resolve it with szr(...) first.

szv() returns an sz object, not a class string. Either pass it to sz=, or wrap it: className={szr(someSzv({ … }))}.

MessageWhen
Thrown. [csszyx] {fn}() received a plain object — the compiler could not resolve this sz prop at build time. followed by Common cause: sz={{ ...(cond ? varA : varB), key: 'val' }} / Fix: sz={[cond ? varA : varB, { key: 'val' }]} / Received: {json}A spread the compiler could not fold reached the runtime.
Thrown. [csszyx] a string helper received an sz OBJECT but the object-lowering module is not loaded, so it cannot be turned into class names.add once at startup: / import '@csszyx/runtime/lowering';Running outside the bundler plugin — unit tests, scripts.

An unreadable szr() argument is reported at build time as szr fallback at … and is always missing output.

You author szs={{ slot: { … } }}; the compiler rewrites the attribute to szsc={{ slot: "class string" }}, which is what the component reads. Every slot must resolve at build time — there is no runtime path.

MessageClass
[csszyx] szs at {filename}: a slot value could not be read at build time, so no slot classes were compiled. Attribute left unchanged. + suggestion Every slot must be an identifier key with a static object literal (or class string) value. For a value only known at runtime, use dynamic() and pass the resulting class string.missing output
[csszyx] szs at {filename}: szs has no effect on a host element — it maps slot names of a custom component. Attribute left unchanged.missing output

“Attribute left unchanged” is literal: the component receives szs, not szsc, and the slots are unstyled.

See Styling Component Parts.

szcn merges class strings and drops the losing side of a conflict. It can only do that for classes it can classify — when a theme token makes a class ambiguous, it falls back to keeping both, and says so once.

Message
[csszyx] theme token "{name}" shadows a built-in {kind} — szcn cannot tell the two apart, so it keeps both classes instead of merging. Both then apply, and stylesheet order decides which wins rather than the order you passed them: a later argument no longer overrides an earlier one. Rename the token — no spelling of the merge can fix this while the name is shared.
[csszyx] theme token "{name}" is defined as BOTH a color and a text size — szcn cannot classify `text-{name}` and will keep-both instead of merging.
[csszyx] theme token "{name}" is defined as BOTH a font family and a font weight — szcn cannot classify `font-{name}` and will keep-both instead of merging.

The fix in all three cases is renaming the token in your @theme block. Keep-both is not a bug — it is the safe answer when the token is genuinely ambiguous.

MessageProduction?
[csszyx] dynamic value on "{key}" is a {type} and cannot be resolved to CSS — the property is omitted. Pass a number, token, or CSS length (for responsive objects, use dynamic()).Yes — this one is deliberately kept in production, because the property is dropped.
[csszyx] dynamic value on "{key}" is a {type} — only a boolean can toggle this class, so it is omitted. Write non-boolean values as literals (or use dynamic() for responsive objects).Yes — same reason. Fires for the boolean-only keys such as border*, ring, outline, shadow and truncate, where a dynamic value can only ever mean on or off. false is a meaningful “off” and stays silent.
[csszyx] dropped an arbitrary value that could inject CSS: "{utility}". Arbitrary values from untrusted data are not emitted at runtime.No — dev only. See Security.
[csszyx] dynamic() has injected {n}+ unique classes this session. Injected CSS rules are never removed, so continuously varying values (e.g. w-[`${x}px`] per animation frame) grow the CSSOM and slow style recalc. Drive continuously changing values through a CSS variable instead.No — dev only, once.
Thrown. csszyx: manifest fetch failed {status}Yes
MessageKind
[csszyx] {n} plugin option(s) are not recognized and have no effect: followed by one line per key — - `{key}`, - `{key}` was replaced by `{renamedTo}`, or - `{key}` — did you mean `{suggestion}`? — and closing with An unread option is silent, so whatever you configured it for is not happening.Warning, fires in production
Thrown. [csszyx] production.mangleMapDelivery must be 'both', 'html' or 'bundle'; got {value}.Error
[csszyx] production.mangleMapDelivery: '{value}' has no effect on the webpack lane — map delivery only narrows on vite/rollup builds.Ungated warning
Thrown. [csszyx] Invalid production.mangleGlobalVars config: + errorsError
Thrown. production.mangleGlobalVars.mode only supports 'alias'. A full rename needs every reference rewritten, including the ones csszyx cannot see.Error, listed under the line above
Thrown. production.mangleGlobalVars.onUnsafeUsage only supports 'error'. An unsafe usage means a token would be aliased where the rename cannot be proven complete, so there is nothing safe to downgrade it to.Error, listed under the line above
Thrown. production.mangleGlobalVars.tokens cannot include Tailwind reserved namespace token {name}Error, listed under the line above
Thrown. production.mangleGlobalVars.tokens cannot include csszyx reserved namespace token {name}Error, listed under the line above
Thrown. [csszyx] production.mangleGlobalVars.enabled requires explicit tokens. Aliasing a property csszyx was not told about would rename references it cannot see.Error
Thrown. [csszyx] production.mangleGlobalVars.autoPrefix is not available: choosing tokens by prefix needs a CSS pre-scan that does not exist yet. List the tokens instead.Error
Thrown. [csszyx] production.mangleGlobalVars validation failed: + one line per findingError
Thrown. [csszyx] CSS variable mangle map is {size} bytes, which exceeds the {max} byte safety cap. Reduce production.mangleVars usage, split the bundle, or raise CSSZYX_VAR_MANGLE_MAP_MAX_BYTES if this payload size is intentional.Error
[csszyx] compileSources: {n} path(s) did not resolve to a directory (relative to {rootDir}): {list}. Their ``sz``/``szv`` will not be compiled or safelisted.Warning
[csszyx] Transform cache disabled because package versions could not be resolved.Ungated, once
[csszyx] production.mangle is not supported by the esbuild adapter; class mangling is disabled so emitted JS and CSS keep matching names.Ungated
⚠️ CSSzyx: Theme Auto-Scan enabled, but TypeScript isn't configured. Run "npx @csszyx/cli init" to fix.Ungated

The unrecognized-option check is worth its own note: csszyx does not silently accept a key it does not read, and it suggests the nearest real key for a typo.

Theme auto-scan without the TypeScript wiring

Section titled “Theme auto-scan without the TypeScript wiring”
⚠️ CSSzyx: Theme Auto-Scan enabled, but TypeScript isn't configured. Run "npx @csszyx/cli init" to fix.

Printed once per process, and the one message on this page with no [csszyx] prefix. Nothing is wrong with your CSS: the scan works, but tsconfig.json does not include the generated .csszyx types, so the theme tokens it finds never reach editor completion or type-checking. Styling is unaffected either way.

Tailwind v4 treats a class name as a namespace several sources contribute to, and when two of them land on the same name it merges the declarations and says nothing. csszyx reports the declaration site, because that is the one place someone can change:

[csszyx] "@utility {name}" collides — {reason}. Tailwind merges both declarations into one rule and reports nothing, so the class carries styles no single place in your CSS shows. Every use is affected, including sz props that spell it correctly. Rename it; if a multi-property class is the point, declare it under a name nothing else claims.

{reason} is either declared twice — two @utility blocks claiming one name — or a theme token already generates it, where a @utility takes a name a --color-* token already produces under one of its prefixes.

There is deliberately no ignore comment. A class that is meant to set several properties has a spelling that costs nothing: a name nobody else claims. An ignore would silence a warning about damage that lands in other files.

Everything that decides which files get compiled and whether the CSS gets generated.

[csszyx] generated {n} sz class(es) but found no CSS entry importing "tailwindcss" — those classes will produce no CSS. Import "tailwindcss" in a CSS file (csszyx auto-injects @source for the generated classes) so Tailwind emits their styles.
MessageNotes
[csszyx] {n} file(s) under packages/ use csszyx but were skipped by ignore rules: + the list + Add the package directory to `compileSources` (or move the file out of packages/) — otherwise their classes never reach the safelist.Normally a nudge. When any skipped file may export szv factories it fires in production too, and adds: {m} of them may export ``szv`` factories, so they stay out of the cross-module registry and every importer falls back to the runtime path.
[csszyx] prescan skipped {file}: the file exceeds the AST node budget, so NONE of its classes reached the safelist and their CSS will not be generated. Raise ``build.astBudgetLimit`` in the csszyx plugin options, or split the file.Ungated
[csszyx] prescan skipped {file}: transform failed, so none of its classes reached the safelist. {error}Ungated
[csszyx] prescan skipped {file}: the file failed to parse, so none of its classes reached the safelist. Fix the syntax error (or check the file extension matches its contents).Ungated
[csszyx] safelist exceeded {max} classes; additional classes were dropped. This usually means an unbounded set of arbitrary values reached an sz prop.Warning
[csszyx] Tailwind content detection is UNSCOPED in a monorepo. Tailwind v4 scans every file under its detection base — .md/.mdx/.txt included — so a doc or fixture holding class-shaped strings becomes CSS, which can generate phantom or broken url() classes and fail the build. That base is as wide as the build makes it: a Vite build roots it at the Vite root, other setups at the workspace root. Scope it in your Tailwind CSS entry:
@import "tailwindcss" source(none);
@source ".";
That @source is your package, relative to the CSS file; csszyx auto-injects one for the classes it generates, so only your own templates need listing. Guide: https://csszyx.com/docs/monorepo-content-scope/
Silence (if a broad scan is intentional): csszyx({ contentScopeCheck: false }).

Full fix: Monorepo & Content Scope.

These are informational and always print to stderr, once.

Message
[csszyx] active parser: rust (native engine) · [csszyx] active parser: wasm (wasm build of the native engine) · [csszyx] active parser: wasm (degraded from default ``rust``: same engine, wasm build)
[csszyx] No prebuilt native binary (@csszyx/core-*) is available for this platform, so the default ``rust`` parser fell back to its wasm build. Same engine, same output; only parse speed differs. To use the native engine, install the matching @csszyx/core-<platform> package (or do not omit optional dependencies). Set ``build.parser`` explicitly to silence this.
[csszyx] parse error in {filename}: the native engine could not fully scan this file ({n} syntax error(s))

One more engine report comes from the browser runtime rather than the build: [csszyx] Failed to initialize WASM core, falling back to JavaScript transformer, followed by the underlying error. The JavaScript transformer produces the same classes, so this costs speed only. Its success counterpart, [csszyx] WASM Core initialized (v{version}), prints through console.info.

MessageKind
Thrown. [csszyx] AST budget exceeded: {filename} has more than {budget} nodes (traversal aborted at {n}). Files this large are almost always machine-generated and should be excluded from sz transformation. Either exclude the file from the plugin (Vite: `csszyx({ exclude: [/large-data\.ts$/] })`), or raise the limit globally with `csszyx({ build: { astBudgetLimit: 100_000 } })`.Caught and printed by the plugin in every mode
Thrown. [csszyx] sz nesting exceeded the maximum depth of {depth}. This usually means untrusted/looping data reached an sz prop.Error
[csszyx] {filename}: source nesting exceeded {max} levels (found {n}) — this usually means accidentally or programmatically over-nested sz/JSX. Flatten the structure. (This guard prevents a parser stack overflow.)Native engine
[csszyx] AST budget exceeded in {filename}: the IR walk stopped mid-file, so the file was left unchanged and contributes NO classes to the safelist. Raise `build.astBudgetLimit` or split the file.Native engine

Mangling replaces class names with short tokens. It is an obfuscation feature — see Plugin Config. Two reports exist, both ungated.

Printed when mangled tokens collide with class names in CSS csszyx does not own, or when a mangled class has no emitted rule:

[csszyx] production mangle found hybrid hazards: {n} mangled token(s) collide with class names in non-csszyx CSS (e.g. …) — those tokens will cross-contaminate external ".{token}" elements. HOTFIX: pass `production: { mangle: false }` to the csszyx plugin to ship now. THEN fix it: if these short names are in your OWN CSS, rename them to something specific (e.g. `.x` → `.resize-handle-x`) — short/common names also clash on specificity with other libraries. Only for names in a third-party stylesheet you cannot edit, list them in `production.mangleExclude` instead. Run `npx @csszyx/cli scan-collisions` to find every offending name.

The orphan clause reads {n} mangled class(es) have no emitted CSS rule (e.g. …) — those elements lose styling. and, when there are no collisions, closes with a different remedy pointing at the CSS pipeline instead.

[csszyx] production.mangle is making this build BIGGER: +{n} B gzipped. The runtime mangle map costs {n} B via {channels}, while the mangled CSS saves {n} B and the shortened classes in code save {n} B. Mangling is a name-obfuscation feature; over a compressed response it does not reduce payload, because utility class names compress far better than the map they need. If you enabled it for size, set `production.mangle: false`. If you enabled it to hide class names, this is the expected price and you can ignore this. Narrowing `production.mangleMapDelivery` removes a map copy when only one channel is needed.

The CSS clause flips to the mangled CSS COSTS {n} B (short tokens compress worse than the names they replaced) when shortening lost.

Also: [csszyx] mangleVars skipped component CSS variable hoist for {name} across {n} usages: {reason} where reason is no-lca, non-host-ancestor, or max-depth.

production.mangleGlobalVars renames custom properties you list, so csszyx refuses any token it cannot prove is yours to rename. Every check below fails the build rather than warning — aliasing the wrong property would silently unstyle whatever else reads it. They arrive together under one error:

[csszyx] production.mangleGlobalVars validation failed:
[{code}] {token} ({file}:{line}:{col}): {message}
MessageMeansFix
Global variable token {name} is not defined in scanned CSS.The token was listed but no scanned stylesheet declares it — usually a typo, or a file outside the scanned setCorrect the name, or widen the CSS the plugin scans
Global variable token {name} is reserved and cannot be aliased.Tailwind owns the name, or your own reserved list claims itDrop it from tokens
Global variable token {name} uses csszyx reserved namespace {prefix}* and cannot be aliased.The name collides with the prefix csszyx allocates aliases fromRename the property in your CSS
Global variable token {name} is declared inside @theme and cannot be aliased.Tailwind generates utilities from @theme tokens, so renaming one breaks themMove it out of @theme, or drop it from tokens
Registered custom property {name} cannot be aliased: @property gives it a type and inheritance behaviour that an alias would not carry.@property gives the name a type and inheritance behaviour an alias would not carryDrop it from tokens
MessageNotes
[csszyx] No checksum found in HTMLThe mangle map was not delivered.
[csszyx] Mangle map script not found
[csszyx] Mangle map failed schema validation (not a plain string→string map); ignoring it.
[csszyx] Mangle map failed schema validation; treating integrity as invalid.
[csszyx] WASM core not loaded — mangle map checksum is unverified (schema-validated only). This is detection, not authentication.Dev only.
[csszyx] Failed to verify mangle map: / Failed to parse mangle map:
[csszyx] Hydration aborted at {tag}: + reason
[csszyx] CSR recovery requires explicit szRecover directiveAdd szRecover="csr".
[csszyx] szRecover="dev-only" is disabled in productionBy design.
[csszyx] Hydration mismatch recovered via CSR. Fix root cause before production.The page renders, but you have a real mismatch.
[csszyx] Stripped {n} szRecover="dev-only" token(s) from the production manifest. Recovery for these elements is disabled in production by design. Sites: {paths}Build-time, informational.
[csszyx] A raw ``sz`` object reached the runtime and was dropped before it could leak to the DOM as sz="[object Object]". + This means the file was not compiled — its `sz` produces no CSS. If it lives in a workspace package, add that package directory to `compileSources`; otherwise check that the bundler is not skipping the file.Dev only, once. The most direct “this file was never compiled” signal there is.

See SSR & Hydration and the Hydration API.

npx @csszyx/cli check runs the same lowering warnings across every file at once and groups them by file — it is the answer to “the dev server only warns about files I happen to open”.

LineMeaning
No sz issues found across {n} files.Clean.
✖ {n} sz issue(s) in {m} file(s).The grouped report follows.
Scope: static sz props and szv()/szr() catalog definitions. Keys that only exist at runtime (an array or spread built from runtime data, a dynamic() value) cannot be checked statically.Printed with every clean run, so the scope is never implied to be wider than it is.
Every one of the {n} emitted class(es) produces CSS under this project's Tailwind{, {m} accepted}.The dead-class check passed. The trailing clause appears only when the baseline accepts known exceptions.
✖ {n} emitted class(es) style nothing. Each is in the DOM and does nothing: fix the sz key, or define the class with Tailwind's @utility.Each offender is listed above it, with the sz key that produced it.
Dead-class check skipped: no stylesheet in this project imports Tailwind, so there is no design system to ask which classes are real.The check needs a Tailwind entry to ask.
Dead-class check skipped: {reason}.
✖ The dead-class check did not run. Its stylesheet is part of this project, so this is reported as a failure rather than a skip — otherwise a check that never runs is indistinguishable from one that found nothing.Exits non-zero. See below.
Files that could not be read: + one line per pathA path given to --files that names a source file the scan could not open.
✖ {n} listed file(s) could not be read, so they were not checked. A pass here would report a subset as if it were the whole list. Check the paths, and note that a separator is normalised rather than trusted, so this is a missing file rather than a Windows path.Exits non-zero, before any file is scanned.
Values that belong to a different sz key: + one line per offender reading {key}: '{value}' emits {class}, which sets {properties} — not what {key} sets.A value written on a key that owns neither it nor its CSS property.
✖ {n} value(s) written on a key that does not own them. Each one compiles, ships CSS and renders, so nothing else reports it; the style asked for is simply absent. Move the value to the key that owns it, or declare a theme token by that name if the spelling was deliberate.Exits non-zero.
Theme tokens a built-in utility already claims: + one line per token reading "{name}" also names {classes}. Tailwind merges both meanings into one rule, so szcn keeps the classes apart instead of merging them and the stylesheet decides which wins — not the order you wrote.Reported at the declaration, with its line.
✖ {n} theme token(s) shadow a built-in utility. Rename them; no spelling of the merge can fix this while the name is shared. To keep one anyway, pass --allow-token <name>.Exits non-zero.

A skip is not always harmless. Two things can stop the dead-class pass, and only one of them is your project’s:

  • Nothing to ask — no Tailwind installed, or a version with no design system. The command skips and passes: failing here would break every consumer who does not build with Tailwind.
  • The question could not be answered — a Tailwind entry was found and did not compile, so the pass was supposed to run and could not. The command skips and fails, because a check that never runs is indistinguishable in CI from one that found nothing, and a project can otherwise stay green for months after its entry stopped compiling.

Several sz keys lower under one Tailwind prefix, so a keyword belonging to one of them compiles cleanly on another: color: 'balance' emits text-balance, which sets text-wrap and no colour at all. The type cannot reject it — a colour may be any theme token — and the class is real, so the dead-class pass is quiet too. The command reads the CSS property the class actually sets and compares it with the one the key sets, which is what separates the mistake from fill: 'none', where the keyword sets exactly the property fill sets.

Your own stylesheet decides. A project that declares --color-balance has given the spelling a meaning, and the report disappears for it.

The pass covers keys whose documented values are theme tokens and nothing else — the colour keys, bg, fontSize, fontFamily and fontWeight. outline, ring and border are left out: each takes a colour and values of its own (outline: 'none', border: '3px'), so a foreign value there cannot be told from an owned one. bg looks like one of them and is not — every other background domain has its own key, bgSize and bgImg among them, so bg: 'cover' is reported.

Declaring --color-balance does not add a colour class. text-balance is already a static utility, so Tailwind merges the two readings and the class ends up carrying text-wrap: balance and the colour — it then competes on color with every other colour class. szcn cannot tell them apart, so it keeps both classes rather than merging, and the stylesheet’s order decides the winner instead of the order the arguments were passed.

That is wrong output rather than a missed optimisation, which is why it exits non-zero. --allow-token <name> keeps a name deliberately, so the exemption is a line in a diff somebody reviews.

The class prefixes each theme namespace feeds are derived from the project’s own Tailwind, not listed — so a colour named cover is reported against bg-cover, a prefix its name gives no hint of.

--files takes the paths a hook hands over, instead of a glob, and --json replaces the prose with one parseable document. The exit code is the same either way, so the format can change without changing what failing means.

lefthook.yml
pre-commit:
commands:
csszyx:
glob: "*.{jsx,tsx}"
run: npx csszyx check --files {staged_files}

Scoping to a subset is sound because the scan lowers each file on its own, with no cross-module registry: a file checked alone yields exactly what it yields in a whole-project run. Paths that are not .jsx/.tsx are dropped rather than refused, so a README staged alongside a component does not fail the hook.

A path that is a source file and still cannot be read is the opposite case, and fails the run. Counting it as scanned is how the command came to print No sz issues found across 1 files for a file it never opened — the one answer a commit gate must never give. Separators are normalised first, so a hook that hands over src\App.tsx on Windows checks that file rather than reporting a clean result for a name that exists nowhere.

The JSON document is { "version": 1, "findings": [...] }, one entry per finding:

{
"rule": "sibling-keyword",
"file": "src/App.tsx",
"line": 12,
"message": "color: 'balance' emits text-balance, which sets text-wrap — not what color sets."
}

rule is one of sz-diagnostic, dead-class, broken-opacity, sibling-keyword, theme-collision. It is a stable id on purpose: messages get rewritten whenever they can be made clearer, so anything filtering on wording would break each time one was.

npx @csszyx/cli scan-collisions finds class names that would collide with a mangled token:

No collision-prone class names found — mangling is safe to enable.
{n} class name(s) could collide with a mangled token:
.{name} (in {where})
Preferred: rename these in your own CSS to something specific
(e.g. `.x` → `.resize-handle-x`) — short names also clash on
specificity with other libraries.
For names in a third-party stylesheet you cannot edit, reserve them:
production: { mangle: true, mangleExclude: ["{name}"] }

npx @csszyx/cli doctor checks the install itself: ✨ No issues found! Your setup looks good., Found {n} issue(s), Tailwind CSS not found, No build output found - run build first, Checksum not found in HTML, and similar.

The VS Code extension reports key problems as squiggles, with its own shorter wording. Turn them off with csszyx.enableDiagnostics: false.

Message
'{key}' was removed: {note}.
Unknown sz prop '{key}'. Did you mean '{suggestion}'?
Unknown sz prop '{key}'. See https://csszyx.com/docs/sz-props for valid props.

The TypeScript plugin emits no diagnostics — it provides completions and hover only. See TypeScript Autocomplete and VS Code Extension.

The Svelte and Vue adapters print a parse failure only when debug is enabled:

  • [csszyx/svelte] Failed to parse sz object: {source}
  • [csszyx/vue] Failed to parse sz object: {source}

csszyx compiles with ONE engine that ships as two artifacts, selected by build.parser: rust (the native addon, the default) and wasm (the same engine compiled to WebAssembly, shipped inside @csszyx/core — also the automatic fallback when the native binary is absent). Because both are compilations of the same source, their classes, code and diagnostics are identical; the frozen parse corpus and a per-PR full-build smoke pin that byte for byte.

Upgrading from a version that still had the TypeScript engines ('oxc' / 'babel' values): those lanes deferred a const the file declares to a runtime CSS variable where the engine reads it at build time (const x = 4 then sz={{ p: x }} now compiles to p-4), and Babel emitted an identifier-named class for a computed key (k-4) where the engine emits none. The engine’s answers were the intended ones; the pinned expectations moved with it.