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 — or a key or value the compiler read emitted a class that styles nothing. 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 |
| A split box will not scroll, clips its corners incorrectly, or leaves its frame visible | Split layout warnings |
| A custom class falls onto the wrong node | Unrecognised classes |
A placement such as outer: ['md:hidden'] does nothing | Base class placement |
pick(cls, 'text:colour') returns nothing | Qualified selectors |
A peer-hover: class never fires inside a split component | Peer rules |
| Warnings stopped appearing part-way through a session | The cap on development warnings |
| 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. |
[csszyx] <{tag}> at {filename}:{line}:{col} carries {n} `sz` attributes; they were merged as sz={[first, …, last]}, later wins per property. — suggestion: fold them into one sz array so the order is written down. | nudge | Write one sz array. Nothing was dropped: every sz compiled, later wins per property, an authored className stays first. |
{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 print in every mode: szr, szs, spreads, budget
bails, and a key or value that emits a dead class.
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 — every kind it holds, not only the fallbacks:
[csszyx] {n} advisory notes not listed above. An advisory reports something csszyx handled — a fallback at an sz prop, a className whose precedence over sz is unstated, or a variable hoist the planner declined — so the styles are there and 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. In CI, run
csszyx check instead: it lists every finding with its file
and line whatever the build mode, and fails the job on them. 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.
Conditional objects
Section titled “Conditional objects”Key and value diagnostics also inspect statically resolved conditional branches, including ternary spreads, whole-object ternaries and resolved items in an sz array. A property shared by both branches is reported once at its source location; two separate declarations are still reported separately. Both branches are checked because either may be used. The diagnostics do not change the emitted classes.
An unresolved spread still reports its runtime fallback. That warning is not a certificate that the literal keys beside it are valid: complete key/value checking of unresolved objects is not supported yet.
| 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] "weight: '{value}'"{ at LOC}: Tailwind spells a numeric font weight through --font-weight-*, so "font-{value}" generates no CSS. Write weight: {value} as a number, which brackets to "font-[{value}]". | A weight written as a string. The NUMBER form brackets for this reason. |
[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}: {value}"{ at LOC} is not a {key} value. The class "{class}" is still emitted and styles nothing, unless a rule of your own happens to match it. {key} takes one of: {values}. | display, position, visibility and isolation only. See below. |
[csszyx] "{key}"{ at LOC} is not a variant, but it holds an object, so it lowers to the class prefix "{key}:" and Tailwind generates no CSS for it. A "--*" key takes a declaration value; "container" takes true. | The class is emitted. Only --* and container are reported — see below. |
[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. |
Four keys spell their value as the class, so a typo is named
Section titled “Four keys spell their value as the class, so a typo is named”display, position, visibility and isolation carry their value as the
bare Tailwind utility — { display: 'flex' } is flex, not display-flex.
That makes an unrecognised value worse than a dead class: it ships verbatim as
a single unprefixed class name, which is the shape a project’s own component
CSS is made of, so { display: 'bogus' } can match a .bogus rule that was
never meant for it.
CSS closes all four value sets, so csszyx names a value outside them, with the class it emitted and the legal set:
<div sz={{ display: 'flex' }} /> // ✅ flex<div sz={{ display: 'none' }} /> // ✅ hidden<div sz={{ display: 'flex!' }} /> // ✅ flex! — the important modifier is a class suffix<div sz={{ display: 'bogus' }} /> // ⚠️ bogus, warned with the legal setThe class is still emitted, on purpose. The diagnostic comes from a static
pass that does not look inside a conditional branch or a parametric variant
such as data: { open: … }; had the build dropped the class instead, a typo
there would vanish with no line to find it by. Emitting keeps the worst case
where it always was — a bare class in the DOM — and the diagnostic covers the
rest.
Anything CSS spells but Tailwind does not is still reachable through the escape
hatch: css: { display: 'ruby' }.
An object under --* or container is not a variant
Section titled “An object under --* or container is not a variant”Any key holding an object becomes a class prefix, which is how a project’s own
@custom-variant and its --breakpoint-* names work without csszyx knowing
them. csszyx therefore cannot tell a typo’d breakpoint from one your CSS
declares, and does not guess.
Two keys are decidable, because csszyx defines what they mean and neither meaning is a variant:
<div sz={{ '--v-x': '0.18' }} /> // ✅ [--v-x:0.18]<div sz={{ '--v-x': { p: 4 } }} /> // ❌ --v-x:p-4 — warned<div sz={{ container: true }} /> // ✅ container<div sz={{ container: { sm: { p: 4 } } }} /> // ❌ container:sm:p-4 — warnedFor a container query, the key is the query itself: { '@sm': { p: 4 } } emits
@sm:p-4.
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" }}, and the component reads that attribute. 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 |
[csszyx] production.mangleMapDelivery has been removed and is ignored. The runtime mangle map is always registered from inside the JS bundle now, on every lane, so the built HTML never carries an executable inline <script> and a strict script-src 'self' policy needs no exception. Delete the option. (\window.__csszyx` is now opt-in through `production.mangleDebugGlobal`.)` | Warning, once per build, whenever the removed option is set |
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.
The safelist moved
Section titled “The safelist moved”The safelist csszyx writes for Tailwind lives at .csszyx/csszyx-classes.txt.
Every producer performs the move and says what it did; these messages come from
the Vite, Rollup and webpack plugins, from csszyx next prebuild and
csszyx next watch, from the Next Turbopack loader, and from the PostCSS plugin.
[csszyx] removed {file}: the safelist now lives at {safelist}. Update any hand-written @source that named the old file, or drop it and list '@csszyx/unplugin/postcss' before '@tailwindcss/postcss' in postcss.config.Printed once per removed file ({file} is csszyx-classes.html or
.csszyx/next-loader-classes.html; {safelist} is .csszyx/csszyx-classes.txt).
Only a file csszyx itself wrote is removed; a file you keep at the old name
stays. Nothing to do unless a stylesheet still names the old file, which the
next message catches.
[csszyx] could not remove {file}: {reason}. Delete it by hand.The new safelist is already written by then; delete the leftover yourself.
[csszyx] {stylesheet}: @source "{target}" names the old safelist, which csszyx no longer writes. The safelist is now {safelist}, and csszyx adds the directive itself: remove this line. Vite, Rollup and webpack inject it; on Next.js list '@csszyx/unplugin/postcss' before '@tailwindcss/postcss' in postcss.config.A build error, not a warning, and it is not gated: a stylesheet pointing at a file that is gone would scan nothing and lose every csszyx class in silence, so the build stops instead. Remove the line.
[csszyx] {config} does not list '@csszyx/unplugin/postcss'. Next.js reads the safelist only through that plugin: list it before '@tailwindcss/postcss', or no sz class gets CSS.Printed by csszyx next prebuild and csszyx next watch. Add the plugin line.
[csszyx] '@csszyx/unplugin/postcss' is listed after '@tailwindcss/postcss'. Tailwind has already compiled the stylesheet by then, so the safelist is never read: list '@csszyx/unplugin/postcss' before '@tailwindcss/postcss'.A PostCSS error: reorder the two plugins.
The safelist could not be written
Section titled “The safelist could not be written”[csszyx] could not write the generated safelist to {path}: {reason}. Tailwind scans that file for the classes csszyx found, so the CSS it generates will be missing them. Check that the directory exists and is writable.The build carries on without those classes, so the page renders with sz props
that produce no CSS. Said once per path — a dev server retries the write on
every edit. A read-only .csszyx, a full disk, or a path owned by another user
all arrive here.
Next.js with Turbopack
Section titled “Next.js with Turbopack”Turbopack compiles each module on its own, so csszyx cannot hold the class set in memory the way it does on the Vite lane. Everything below is a build stop rather than a warning: carrying on would ship a page whose classes have no CSS.
[csszyx] Next Turbopack production cache is not ready for {root}: {reason}. Production builds with Turbopack need the csszyx safelist seeded first: npx csszyx next prebuild 'app/**/*.tsx'. Wire it into package.json so plain builds keep working: "build": "csszyx next prebuild 'app/**/*.tsx' && next build". Docs: https://csszyx.com/docs/installation#nextjs-turbopack-setupPrinted across several lines in the terminal. Run the prebuild once, then keep
the build script the message names so the next next build finds the cache
already seeded.
[csszyx] Next Turbopack does not support production CSS variable mangling for {file}. Use Next Webpack mode for full csszyx parity.[csszyx] Next prebuild does not support production CSS variable mangling. Use Next Webpack mode for full csszyx parity.Mangling rewrites variable names across the whole bundle, which needs a view of every module at once. Turbopack does not offer one; the Webpack lane does.
[csszyx] Next source transform failed closed for {file}: source still contains csszyx sz syntax.The transform ran and left sz behind, which would reach the browser as an
unknown prop. Stopping is the safe answer, and the file is named because the
input that produced it is worth seeing.
[csszyx] Next safelist watcher failed to run its initial cycle.The watcher could not complete its first pass, so nothing after it can trust the
safelist. Usually a permissions or path problem under .csszyx.
[csszyx] next watch is waiting for the safelist lock held by the Turbopack loader (process {pid}). Classes added meanwhile get no CSS until it is released; a lock left by a loader that exited is recovered within 30 s.Unlike the stops above, this one is a notice: the watcher keeps running. next watch and the Turbopack loader share one lock, and the loader holds it for about
a millisecond at a time, so an overlap normally clears on the next retry without
a word. This prints once when the wait passes a second, which in practice means
the loader was killed while holding the lock. The wait ends on its own when that
lock goes stale. If it lasts more than a minute, the watcher stops and reports the
lock instead.
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.
A stylesheet that matches classes by their text is the third hazard. The clause
names the selector and the classes it was matching, and the remedy is a
paste-ready production.manglePreserve:
[csszyx] production mangle found hybrid hazards: {n} attribute selector(s) match class names by text (e.g. …) — those rules stop matching once the classes are renamed, so the elements lose those styles. Keep those classes readable with production.manglePreserve: [{entries}] (paste-ready), or key the rule off a data attribute, which mangling never touches. production.mangleExclude cannot help here: it reserves token names and does not keep a class from being renamed.The sample after e.g. is each selector followed by the classes it was
matching, and the paste-ready value is a prefix ('bg-tag*') when every renamed
class starts with the selector’s value, the exact names otherwise. When a selector
such as [class^="y"] in external CSS would begin matching the tokens
themselves, the clause reads {n} attribute selector(s) would start matching mangled tokens instead (e.g. …). followed by its own remedy: Reserve those token names with production.mangleExclude: [{tokens}] (paste-ready) so no class is renamed to one of them, or key the rule off a data attribute, which mangling never touches; a prefix or substring selector goes on matching other tokens, so the data attribute is the durable fix.
A manglePreserve entry matched nothing
Section titled “A manglePreserve entry matched nothing”[csszyx] production.manglePreserve: {n} {entries} matched no csszyx class in this build ({list}) — nothing was preserved for them. Check the spelling against the class census; an entry keeps a class only when its name is exact, or when it ends in `*` and the class starts with the rest.Printed once per build. A silent no-op here is the defect the option exists to fix, so the build says which entries did nothing.
manglePreserve rejects an entry
Section titled “manglePreserve rejects an entry”Both fail when the plugin is created, before any build runs:
[csszyx] production.manglePreserve[{index}] must be a non-empty string (an exact class name, or a prefix ending in `*`); got {value}.[csszyx] production.manglePreserve must not contain a lone `*`: it would keep every class and silently turn `production.mangle` into a no-op. Name a prefix (`bg-tag-*`) or set `production.mangle: false`.A RegExp is rejected on purpose: a stateful flag makes a match alternate across
the census, a RegExp serialises to {} in the config hash, and a pathological
pattern can stall the build for seconds per class.
A mangleExclude name that can never be a token
Section titled “A mangleExclude name that can never be a token”[csszyx] production.mangleExclude: {n} {names} can never be a mangle token ({list}) — tokens are short base62 strings, so those entries do nothing. To keep a class from being renamed, list it in `production.manglePreserve` instead.Tokens never contain -, _ or :, so a class name listed under
mangleExclude was almost certainly meant for manglePreserve.
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. Two copies is not a misconfiguration: the bundle carries the map the runtime reads, and the HTML carries the same census for the hydration checksum.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. It is a usage nudge: the planner declined an optimisation and
every class and variable is still emitted, so a production build holds the line
back and counts it in the advisory total instead.
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 |
Alias plan mismatch
Section titled “Alias plan mismatch”[csszyx] production.mangleGlobalVars validation failed: CSS alias plan {actual} does not match source-transform alias table {expected}.The names the stylesheet was rewritten with and the names the source transform recorded have to be the same set. When they are not, some variable would be read under one name and written under another, so the build stops rather than ship a page whose custom properties resolve to nothing. Both tables are printed; the difference between them is the bug.
SSR & hydration
Section titled “SSR & hydration”| Message | Notes |
|---|---|
[csszyx] No checksum found in HTML | The page claims no build. Every production build writes the attribute. |
[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”. Pass a directory — npx @csszyx/cli check src —
to scan only the files under it; the stylesheet and Tailwind are still found
from the project root, and --pattern is read relative to that directory.
| Line | Meaning |
|---|---|
No sz issues found across {n} files. | Clean. |
No selected sz issues across {n} files; {m} left out by --rule or --ignore-rule. | Every issue found was left out by the rule selection. The run passes, and the line says it was not clean. |
{m} more sz issue(s) left out by --rule or --ignore-rule. | Printed under the report when the selection left some issues out. |
✖ {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. |
✖ A directory and --files both choose the files to check. Pass "{dir}" or --files, not both. | Exits non-zero, before any file is scanned. |
✖ "{dir}" is a file, not a directory. To check single files, pass --files {dir}. | Exits non-zero, before any file is scanned. |
✖ --pattern "{pattern}" is an absolute path, so it would not stay inside "{dir}". Pass a pattern relative to the directory. | Exits non-zero before scanning: an absolute pattern ignores the directory it was given. |
✖ "{dir}" does not exist under {cwd}, so there is nothing to check. | Exits non-zero rather than passing on the zero files a missing directory would match. |
✖ {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. That comparison 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, so the command 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.
generate-types and Tailwind v3
Section titled “generate-types and Tailwind v3”generate-types reads a v3 tailwind.config.js through Tailwind’s own resolveConfig. Tailwind v3 is an optional peer of @csszyx/cli — 12 MB across 37 packages that only this command touches — and no package manager warns when an optional peer is missing, so the command checks before it reads a config — and before it reports that it found none, since a v4 project has none by design — and stops with one of three messages. Each is the whole explanation, printed under a ❌ prefix.
No Tailwind at all:
generate-types needs Tailwind CSS v3, and this project has none installed. This command turns a v3 JavaScript config into TypeScript declarations, and reading that config is Tailwind's own job — the command calls Tailwind's resolveConfig to do it. With no Tailwind installed there is nothing to call, so it stops here rather than guessing at your theme. Install it next to the config: npm install -D tailwindcss@3 / pnpm add -D tailwindcss@3 / yarn add -D tailwindcss@3. Why it was not installed for you: Tailwind v3 pulls in 12 MB across 37 packages, and generate-types is the only csszyx command that touches it. Shipping it as a hard dependency would put those 12 MB into every install of @csszyx/cli — including a CI runner that only ever runs csszyx check. It is declared as an optional peer instead, so it arrives when you ask for this command and not before. If your project is on Tailwind v4 there is no tailwind.config.js for this command to read: v4 moved the theme into CSS (@theme { … }). Do not install v3 to get past this message — you do not need generate-types at all.Tailwind is installed, but it is v4 (or any major other than 3):
generate-types needs Tailwind CSS v3, and this project has {version}. Nothing is broken. This command exists to read a v3 JavaScript config (tailwind.config.js) out of an older project, and Tailwind v4 removed both that config format and the resolveConfig helper the command calls. There is no version of generate-types that works against a v4 install. On v4 the theme lives in CSS and needs no generated declarations: @import "tailwindcss"; @theme { --color-brand: oklch(0.7 0.15 250); }. If you are part-way through migrating and still have a v3 tailwind.config.js you want typed, run the command in an environment that has v3 rather than downgrading this project: npx -p tailwindcss@3 -p @csszyx/cli csszyx generate-types --config ./tailwind.config.js. Why csszyx did not install v3 for you: it is 12 MB across 37 packages for one command, so it is an optional peer rather than a dependency.v3 is installed and its entry did not load — a patched copy, a blocked exports map, a damaged file:
generate-types found Tailwind CSS {version} but could not load its resolveConfig entry: {reason}. The version is right, so this is not a missing install — the package is there and the entry point it advertises did not load. A reinstall usually clears it: npm install --force tailwindcss@3 / pnpm add -D tailwindcss@3. If it persists, the config cannot be read and generate-types has nothing to generate from. Open an issue with the version above and this line.The same three sentences are thrown as an Error from scanTailwindConfig when it is called as a library, so a host such as the MCP server shows the explanation rather than a bare resolver error. csszyx doctor reports the state under 🧰 Optional tooling as a line that never counts as an issue: generate-types available (tailwindcss {version}), generate-types unavailable — tailwindcss v3 is an optional peer and is not installed. Only needed to read a v3 tailwind.config.js., generate-types unavailable — tailwindcss {version} has no JavaScript config to read. Not needed on v4., or — as a warning, since a reinstall is due — generate-types unavailable — tailwindcss {version} is installed but its resolveConfig entry did not load: {reason}. Reinstall it: npm install --force tailwindcss@3.
What check reports
Section titled “What check reports”[csszyx] ✖ {n} emitted class(es) carry an opacity modifier that does not survive compilation.A theme token that resolves to a bare RGB triplet cannot be dimmed by
color-mix(), so the opacity is dropped from the compiled CSS while the class
name still carries it. The lines above this one name each class and its origin;
wrap the variable, for example rgb(var(--your-triplet)).
[csszyx] ✖ {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.The run exits non-zero rather than reporting a clean pass over the files it did manage to read.
csszyx next-watch refuses to start on a bad interval rather than fall back to
a default the command line did not ask for:
Invalid --debounce-ms. Expected an integer between 0 and 60000.
Running it in CI
Section titled “Running it in CI”csszyx check is the command to gate CI on. A production build is the build CI
usually runs, and it prints advisory notes only as a count; check lowers every
source file the same way the build does, lists each finding with its file and
line, and exits non-zero while any remain, whatever NODE_ENV says.
- name: csszyx run: npx csszyx check --ignore-rule class-precedence --ignore-rule duplicate-szThe two ignored kinds report styles that are present: class-precedence says
sz wins over a runtime className, and duplicate-sz says which order two
sz attributes merged in. Drop an --ignore-rule to fail on that kind too, or
use --rule to gate on a chosen set only. Add --json when a later step reads
the findings, for example to annotate a pull request; the exit code is the same.
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.", "kind": "sibling-keyword"}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.
kind says what an sz-diagnostic found: unknown-key, canonical-key,
removed-key, numeric-key, closed-enum-value, off-scale-value,
numeric-font-weight, per-side-border-style, property-object,
non-variant-object, unknown-field, runtime-value, unresolvable-spread,
style-override, szs-slot-map, sz-recover, class-precedence,
duplicate-sz, or other for a message no kind matches. Every other pass
repeats its rule. Runtime fallback notes are not check findings, so they
have no kind.
An unknown-key finding also carries suggestion when one known key is a
single edit away — two for a key of twelve letters or more — and the prose
report, the build and the dev server print Did you mean "{key}"? under the
diagnostic. A swap of two neighbouring letters counts as one edit, and an alias
suggests its canonical key, so workBreak suggests break. Nothing is
rewritten, and a key with no single near match gets no suggestion, so a class a
project serves with @utility is not second-guessed.
--rule <id> keeps only the findings of that rule or kind, and
--ignore-rule <id> leaves them out; both repeat, and both change the exit code
as well as the report. A gate that fails on key mistakes but not on precedence
notes is csszyx check --ignore-rule class-precedence. An id that is neither a
rule nor a kind fails the run rather than selecting nothing:
[csszyx] ✖ {given} is not a rule or a diagnostic kind, so it would select nothing. Known ids: {ids}.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} (selector declared in {where})
These files declare the selectors; the class may be emitted elsewhere, such as a shared component, so find where the class is emitted before renaming.
Preferred: rename the class where it is declared and where it is emitted, 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}"] }The scan reads stylesheets only, so the files it lists are where each selector is declared. In a monorepo the class is often emitted by a component in another package; rename it there too, or the component keeps emitting a name the renamed rule no longer matches.
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.
Runtime helpers
Section titled “Runtime helpers”Errors from @csszyx/runtime, met while a page runs rather than while it builds.
[csszyx] a string helper received an sz OBJECT but the object-lowering module is not loaded, so it cannot be turned into class names. The csszyx bundler plugin loads it automatically for files that can pass objects at runtime. Outside the plugin pipeline (unit tests, scripts), add once at startup: import '@csszyx/runtime/lowering';The message names the fix. It appears in unit tests and standalone scripts, where nothing has run the plugin that would have injected the import.
[csszyx] {helper}() received a plain object — the compiler could not resolve this sz prop at build time. Common cause: sz={{ ...(cond ? varA : varB), key: 'val' }} Fix: sz={[cond ? varA : varB, { key: 'val' }]} Received: {value}A spread of a conditional hides the shape from the compiler, so the build has nothing to lower. Passing an array keeps both branches visible and csszyx merges them.
The class toolkit refuses a selector it cannot act on, in development, and answers as if nothing matched:
[csszyx] has/pick/omit take one selector, not an array; pass the selectors one at a time, or use splitBox whose inner/outer options take a list.[csszyx] an empty selector {} matches nothing; name a category and value, e.g. { overflow: "hidden" }.[csszyx] '{selector}' is not a category or class prefix csszyx knows; the category is '{hint}'.When the word is also a property the qualified form can name — color is
the usual one — the hint offers that form beside the category, because the
category alone would catch more than the word meant: 'text' also matches
text-sm.
[csszyx] '{name}' is not a category or class prefix csszyx knows; the category is '{hint}', and '{hint}:{name}' matches that property only.[csszyx] '{selector}' is not a category or class prefix csszyx knows; classify('<a class>') shows the category a class belongs to.[csszyx] an object selector names one category and value; { {categories} } can never match a single token.An empty object used to match every csszyx token, and a misspelt category
answered false in a way that read as “no such class”. The unknown-name
message carries the category when the word is a CSS property people reach for
(width and height are sizing, color is text, background is bg,
cursor is interaction); for any other word, classify('p-4') and friends
show which category a class belongs to. The same checks run for hasSz,
pickSz, omitSz and splitBoxSz, where an sz key such as minW is a valid
selector.
Two helpers take two different inputs, and mixing them up says so:
splitBoxSz partitions sz objects, not raw class strings — use splitBox() for {value}.
splitBox takes a class string; splitBoxSz takes an sz object.
A qualified selector reached the sz twins
Section titled “A qualified selector reached the sz twins”[csszyx] '{selector}' names a property, which the sz twins do not read: an sz key has no value to classify. help: pass '{name}'.'text:color' qualifies a class selector by the property its value names —
text-red-500 is a colour, text-sm a size. An sz key is a property already
(color, text), so there is no value to classify and the form means
nothing on hasSz, pickSz, omitSz and splitBoxSz. The message says what
to pass instead of pretending the selector does not exist.
A peer rule kept a class on the frame
Section titled “A peer rule kept a class on the frame”[csszyx] splitBox: '{token}' stays on the frame although '{base}' belongs inside, because a peer rule reaches siblings and the content node is a child of the frame, where it could never match. help: to reach the content instead, target it from the frame: '{reaching}'.Every peer-* variant compiles to the general sibling combinator — the rule
for peer-hover:p-4 matches an element that is a following sibling of the
hovered .peer. The frame is that sibling; the content node is a child of the
frame, so a rule placed there can never match, and not-peer-* there is worse:
the negation of a match that cannot happen is always true, so the class is
permanently on. splitBox therefore keeps every peer-* utility on the frame
whatever side its base belongs to, and says so once when that moved it —
peer-checked:bg-red-500 was going outside anyway and stays quiet.
The help line names the one way to get the effect onto the content: an
arbitrary child variant on the frame, peer-hover:[&>*]:p-4, which Tailwind
compiles to the sibling rule followed by > *. group-*, has-* and in-*
are not affected — their rules reach descendants, and the content node is one.
A qualified selector names a property csszyx does not tell apart
Section titled “A qualified selector names a property csszyx does not tell apart”[csszyx] '{property}' is not a property csszyx tells apart; '{selector}' matches nothing. help: the properties are {list} — classify('<a class>') shows the one a class carries.A selector such as 'text:color' reads the half after the colon as a
property, and the toolkit only knows the eighteen it distinguishes itself:
align, attachment, clip, color, direction, family, image,
origin, overflow, position, repeat, shorthand, size, style,
thickness, weight, width and wrap. Anything else — colour, paint,
fontSize — would have matched nothing silently, which is worse than the
category half being wrong, because that half already warns. The message
lists the set so the fix is a lookup rather than a search.
A split that cannot do what the className asked for
Section titled “A split that cannot do what the className asked for”splitBox routes every token correctly and the result still cannot work, in
three shapes. Each is silent in the DOM — the classes are all present — so the
warning is the only place it is said. Development only, once per message, and
none of them changes the output.
[csszyx] splitBox: '{token}' went to the content node, but nothing bounds the height of either node, so the content will grow instead of scrolling. help: give the className a height bound such as h-64, max-h-96 or h-full, or put 'flex flex-col min-h-0' on the frame and 'flex-1 min-h-0' on the content.A scroll container with no height grows to fit its content, so it never
scrolls. The bound can be a class on either node (h-*, max-h-*, min-h-*,
size-*) or it can come from the parent, which is why an absolutely positioned
frame and a flex child (flex-1, grow, basis-*) are accepted as bounded.
[csszyx] splitBox: the frame is rounded and the content scrolls, but the frame does not clip, so scrolled content paints over the corners. help: add 'overflow-hidden' to the frame.Scrolled content paints out to the padding box, so it runs over a corner the frame rounded but never clipped.
[csszyx] splitBox: '{token}' went to the content node, so the frame keeps its background, border and size and stays visible. help: pass { outer: ['hidden'] } if the whole box should disappear.hidden is display: none, which acts on the contents, so it stops the content
node from rendering while the frame keeps painting. Under a variant the pair is
usually deliberate — hidden md:block toggles the content on purpose — so only
the bare form is named.
These three read the class buckets, so they do not fire for splitBoxSz, which
partitions an sz object.
A class csszyx does not recognise
Section titled “A class csszyx does not recognise”[csszyx] splitBox: '{token}' is not a utility csszyx knows, so it went to the {node} node with everything else it could not classify. help: if it is a custom @utility that declares properties for both nodes, csszyx cannot split it — no side is correct — so place it yourself with { {other}: ['{token}'] }.The toolkit’s vocabulary is atomic utilities — a class whose name states one
feature. A custom @utility that declares several properties at once has no
correct side: send it outward and the content loses the padding it declared,
inward and the margin moves inside the frame, to both and the margin applies
twice while the background paints twice. csszyx will not rewrite your CSS into
per-property fragments to make the question answerable, so it says what it did
and hands the call back.
{node} is the node the fallback chose — frame by default, content when
you passed fallback: 'inner' — and {other} is the opposite one, so the help
line always suggests moving the class rather than leaving it where it is.
Placing the token by hand is the fix, and a placement list accepts the literal
class name for exactly this case — { inner: ['card'] } works even though
card is in no category. It is matched as a whole name, never as a prefix of
card-lg, because csszyx knows nothing about an unrecognised token’s
structure, and it is the name you WROTE even on a mangled build, since the
token is decoded before the comparison.
A placement list therefore does not report an unknown base name: it can name
a literal class, and one options object usually serves many classNames, so a
render without that class is not a mistake. has, pick and omit are the
other way round — there the string IS the query, so an unknown one is still
reported as the typo it usually is.
The message also never fires for a token you placed yourself, including onto the side the fallback would have chosen anyway.
A placement names a variant
Section titled “A placement names a variant”[csszyx] splitBox: a placement list names a class by its base, so '{token}' never matches; the variant prefix and the ! or - marker are stripped before the comparison. help: write { {side}: ['{base}'] }; it places every variant of '{base}'.Use the base name in outer and inner placement lists. The rule applies to
every variant of that base, so { outer: ['hidden'] } also places
hover:hidden and bare hidden on the frame. Selecting only md:hidden
is not supported, and neither is the important or negative form — !hidden
and -mt-4 normalise to hidden and mt-4 before the comparison, and the
message names that form. This development warning leaves routing unchanged;
update the placement to make it take effect.
See Override the defaults for literal custom-class placement, and Categories for recognised selectors.
The cap on development warnings
Section titled “The cap on development warnings”[csszyx] {count} distinct development warnings have been printed; further ones are suppressed for this session. help: a className built from data is the usual cause — look above for one warning repeating with different values.Every warning prints once per distinct message, and the cache behind that
holds at most 512 of them. Several messages interpolate a class name from your
own className, so a class built from data — user-data-42, one per row —
would otherwise make every render a new message and the cache grow for as long
as the dev server runs. Rather than go quiet when it fills, the cache says so,
once: the line above replaces the first message it would have dropped. A
warning that repeats above it with different values names the component to
look at. Production is unaffected; no warning is emitted there at all.
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.