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.
How to read a csszyx message
Section titled “How to read a csszyx message”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.
Anatomy of a fallback line
Section titled “Anatomy of a fallback line”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().| Part | Meaning |
|---|---|
sz fallback | The site. One of sz fallback, szr fallback, szv catalog. |
12:9 | Line and column of the expression, 1-based. |
| reason | What could not be read — the expression shape. |
Suggestion: | The way out, chosen by the expression shape. |
The two classes of build warning
Section titled “The two classes of build warning”This is the single most useful distinction in this page.
| Class | Means | Dev build | Production build | quiet: 'nudges' | quiet: true |
|---|---|---|---|---|---|
| missing output | Classes never reached the safelist, so no build step was told to generate their CSS. The markup names utilities that do not exist. | prints | prints | prints | silent |
| usage nudge | The classes were collected. The message is advice for whoever writes csszyx code. | prints | silent | silent | silent |
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.
Every silencing switch
Section titled “Every silencing switch”| Switch | Silences | Where |
|---|---|---|
quiet: 'nudges' | Usage nudges, keeps missing-output reports | Plugin option |
quiet: true | All plugin warnings (errors still throw) | Plugin option |
contentScopeCheck: false | The unscoped-monorepo warning only | Plugin option |
CSSZYX_QUIET_SZ_WARNINGS=1 | The sz key/value dev warnings (§ sz key and value warnings) | Environment |
CSSZYX_NO_PROJECT_SCAN_HINT=1 | The one-time csszyx check tip | Environment |
csszyx.enableDiagnostics: false | VS Code squiggles | Editor setting |
NODE_ENV=production | Every runtime dev warning (they are dead-code-eliminated) | Environment |
See Plugin Config → Quiet for the option itself.
Quick index
Section titled “Quick index”| What you actually see | Start here |
|---|---|
| An element has no styling at all | sz fallback, safelist and CSS entry |
| One property does nothing, the rest works | sz key and value warnings |
sz="[object Object]" in the DOM | szr and the string helpers |
class="[object Object]" | szv runtime |
| Styles work in dev, break in production | Mangling, SSR and hydration |
A variant (hover:, md:) never applies | sz key and value warnings |
| Slots on a component are unstyled | szs and szsc |
| Merging two class strings keeps both | szcn |
| The build got slower or bigger | Mangling |
| Only some files are styled | Build pipeline |
sz — unresolved expressions
Section titled “sz — unresolved expressions”These fire when the compiler cannot read the value you put in sz={…}. All
both engine artifacts emit identical wording.
What each kind means:
| Kind | Triggered by | Classes still collected? | Fires in production? |
|---|---|---|---|
call | sz={makeSz()} | Yes — dynamic() and friends compile correctly | No (nudge) |
identifier | sz={styles} where styles is not a readable const | Yes, when a caller supplies it | No (nudge) |
import | sz={imported} from another module | No | Yes |
member | sz={theme.card} | Yes, when a caller supplies it | No (nudge) |
szv-factory | sz={myVariants({ … })} where the compiler saw myVariants = szv(…) and refused its config | Yes — the runtime szv path renders | No (nudge) |
other | anything else — a template literal, a conditional | Yes | No (nudge) |
Exact wording — {detail} is the callee name, identifier name, or node
type:
| Kind | Reason | Suggestion |
|---|---|---|
call | function call `{detail}()` result is unknown at build time | If it returns static variants → convert to szv(). If it depends on runtime data → use dynamic(). |
identifier | identifier `{detail}` could not be resolved to a static value | 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(). |
import | imported binding `{detail}` could not be read at build time | Export 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(). |
member | member expression is not statically resolvable | Extract the value to a module-level const. For variant-based styling → szv(). For true runtime values → dynamic(). |
szv-factory | szv 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(). |
other | expression of type `{detail}` is not statically analyzable | Use 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.sz — other build diagnostics
Section titled “sz — other build diagnostics”| Message | Class | Fix |
|---|---|---|
[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 output | Use 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(). | nudge | Optional. 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. | nudge | Pass 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. | nudge | Use a literal. |
[csszyx] szRecover at {filename}: unknown mode "{value}" — expected "csr" or "dev-only". Token emission skipped. | nudge | Use "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. | nudge | State 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 output — szr, 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.
sz — key and value warnings
Section titled “sz — key and value warnings”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.
| Message | Notes |
|---|---|
[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.
Values that generate no CSS
Section titled “Values that generate no CSS”| Message | Notes |
|---|---|
[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 ruleborder-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.
Keys that need a build-time value
Section titled “Keys that need a build-time value”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 allcolor 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.
What you get now
Section titled “What you get now”<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.
Write it one of these three ways
Section titled “Write it one of these three ways”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 helperAn open-ended value from data — dynamic(). Only when the
value genuinely is not one of a known set; it injects CSS in the browser.
The full list
Section titled “The full list”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 page | Keys |
|---|---|
| Backgrounds | bgAttach bgClip bgImg bgOrigin bgRepeat bgSize |
| Borders | borderStyle outlineStyle |
| Effects | maskClip maskComposite maskConic maskLinear maskMode maskOrigin maskRepeat maskType mixBlend |
| Flex & Grid | alignContent flexDir flexWrap gridFlow items justify justifyItems justifySelf placeContent placeItems placeSelf self |
| Interactivity | appearance fieldSizing pointerEvents resize scheme scroll scrollbar scrollbarGutter select snapAlign snapStop snapType touch |
| Layout | box boxDecoration breakAfter breakBefore breakInside clear display float isolation notSrOnly objectFit overflow overflowX overflowY overscroll overscrollX overscrollY position srOnly visibility |
| Misc | borderCollapse caption forcedColorAdjust tableLayout |
| Sizing | container |
| Transforms | backface transformStyle |
| Transitions | transitionBehavior |
| Typography | decoration 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.
Colors
Section titled “Colors”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} }. |
Mask layers
Section titled “Mask layers”| Message | Notes |
|---|---|
[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. |
szv — build
Section titled “szv — build”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.
szv — runtime
Section titled “szv — runtime”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({ … }))}.
szr & the string helpers
Section titled “szr & the string helpers”| Message | When |
|---|---|
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.
szs & szsc
Section titled “szs & szsc”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.
| Message | Class |
|---|---|
[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.
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.
dynamic()
Section titled “dynamic()”| Message | Production? |
|---|---|
[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 |
Plugin configuration
Section titled “Plugin configuration”| Message | Kind |
|---|---|
[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: + errors | Error |
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 finding | Error |
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.
A class name with more than one author
Section titled “A class name with more than one author”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.
Build pipeline
Section titled “Build pipeline”Everything that decides which files get compiled and whether the CSS gets generated.
No CSS at all
Section titled “No CSS at all”[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.Some files skipped
Section titled “Some files skipped”| Message | Notes |
|---|---|
[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 |
Monorepo content scope
Section titled “Monorepo content scope”[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.
Which parser ran
Section titled “Which parser ran”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.
Budgets and limits
Section titled “Budgets and limits”| Message | Kind |
|---|---|
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
Section titled “Mangling”Mangling replaces class names with short tokens. It is an obfuscation feature — see Plugin Config. Two reports exist, both ungated.
Hybrid hazards
Section titled “Hybrid hazards”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.
Mangling made the build bigger
Section titled “Mangling made the build bigger”[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.
Global variable aliasing
Section titled “Global variable aliasing”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}| Message | Means | Fix |
|---|---|---|
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 set | Correct 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 it | Drop 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 from | Rename 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 them | Move 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 carry | Drop it from tokens |
SSR & hydration
Section titled “SSR & hydration”| Message | Notes |
|---|---|
[csszyx] No checksum found in HTML | The 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 directive | Add szRecover="csr". |
[csszyx] szRecover="dev-only" is disabled in production | By 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”.
| Line | Meaning |
|---|---|
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 path | A 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.
Running it as a git hook
Section titled “Running it as a git hook”--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.
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.
Editor diagnostics
Section titled “Editor diagnostics”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.
Framework adapters
Section titled “Framework adapters”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}
One engine, two artifacts
Section titled “One engine, two artifacts”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.