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] {module}:12:9 sz fallback at 12:9: identifier `cardStyles` could not be resolved to a static value. Suggestion: make it a module-level or function-body `const` with a literal object value; for variant-based styling use `szv()`, for runtime values `dynamic()`.| Part | Meaning |
|---|---|
{module}:12:9 | The module the bundler named, such as /src/components/Card.tsx, and the line and column the engine placed the finding at. Every sz build diagnostic starts with this header. |
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 |
The two classes are the default levels of
csszyx.config: missing output is
error (or warn), a usage nudge is info. Setting a level moves a finding
between them — see Levels in a build.
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 |
diagnostics.rules: { '<id>': 'off' } | One kind of finding, on every lane and in csszyx check | csszyx.config (.ts, .mts, .js or .mjs) |
contentScopeCheck: false | The unscoped-monorepo warning only | Plugin option |
CSSZYX_QUIET_SZ_WARNINGS=1 | The sz key/value warnings of the runtime, the rows marked every mode included (§ sz key and value warnings); on the Next Turbopack loader, which takes no quiet option, what quiet: true mutes (§ Turbopack) | 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), except the sz rows marked every mode | Environment |
See Plugin Config → Quiet for the option itself.
Levels in a build
Section titled “Levels in a build”Each sz build diagnostic carries the id csszyx check reports it under —
unknown-key, fallback-nudge, ast-budget and the rest — and its level comes
from csszyx.config for that id, in
that file. The id decides, not the wording: a reworded message stays where it
was.
| Lane | off | info | warn / error |
|---|---|---|---|
Vite dev server, development build, webpack --mode development | silent | listed with file and line | listed with file and line |
| Production build (Vite, webpack, Rollup, esbuild) | silent | counted in the closing line | listed with file and line |
Turbopack loader (next dev / next build) | silent | listed under next dev, dropped under next build | listed |
| jest transform | silent | dropped | listed |
A build never fails on a level; csszyx check is the gate. quiet: true mutes
every level. quiet: 'nudges' holds info back and keeps warn and error.
The Turbopack loader cannot load the config file, so it reads the levels
csszyx next prebuild or csszyx next watch resolved into
.csszyx/diagnostic-policy.json; without that file it uses the recommended
levels. Jest reads the same file, and without it loads the config itself, once
per worker, printing its problems the way a build does. The file lists the
config and every module it imports by a relative path; once one of them changed
since the file was written, jest and the loader both load the config
themselves. A shared preset imported as a package is not listed: run
csszyx next prebuild again after updating it. A Vite dev server, every watch build (webpack, rollup -w, vite build --watch,
an esbuild context) and csszyx next watch read the config again when it, or a
module it imports by a relative path, is saved, and print each finding again at
its new level. Run csszyx next prebuild again before next build.
Jest loads the config the first time a file has something to report, so a fully
cached run never loads it. When a worker cannot read the levels, it says so and
keeps the levels of .csszyx/diagnostic-policy.json, or recommended without
that file; {lane} is jest or the Turbopack loader:
[csszyx] config-unloadable: {config} This file could not be loaded in {lane}, so the levels in `.csszyx/diagnostic-policy.json` apply, or the `recommended` levels when that file does not exist. help: make the file load in plain Node; `csszyx next prebuild` and `csszyx next watch` write `.csszyx/diagnostic-policy.json` for {lane}. note: the loader reported: {reason}. docs: https://csszyx.com/e/config-unloadable{reason} is it did not finish loading within 30 s for a config that never
settles — a server or a timer started at the top level; raise the wait with
CSSZYX_JEST_CONFIG_TIMEOUT_MS — or the config loader printed no result for
one that ends the process (process.exit()) while it loads. A config that
throws is reported with its own error instead.
Each site prints once. A dev server compiles a module again when a stylesheet or another module changes; a finding already printed for that file is not printed again until the file itself changes — an undo back to an earlier version counts as a change, so a finding a fix removed and the undo brought back is printed again. The Turbopack loader is the exception: Next keeps its warnings per module and drops one a re-run no longer reports, so the loader reports every run and Next deduplicates.
At most ten lines per id. After ten lines of one id in a start or a build — on a dev server, in one burst of compiles — the rest are counted and one line closes them:
[csszyx] diagnostics-capped: This build left {count} more `{id}` finding(s) unlisted. help: `csszyx check --rule {id}` lists every one.The sentence names the capped id, so a search for unknown-key finds the count
beside the ten it lists (diagnostics-capped).
A build prints it at its end; a dev server, the loader and jest print it once the burst of compiles that produced them has paused for a second, or as the process exits, whichever comes first. The count is of findings still there: one an edit fixes or moves before it prints is dropped from it. The next burst starts a fresh count, so a new finding later in a dev session is listed with its file and line.
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 |
The build stops with set different prefixes | Tailwind entries set different prefixes |
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) |
variant | sz={{ m: 1, hover: x }} — one variant key whose value only the runtime knows, in an object or an sz array element; a key no table names (tablet: x for a project breakpoint) counts as a variant | No | Yes |
Exact wording — {detail} is the callee name, identifier name, or node
type:
| Kind | Reason | Suggestion |
|---|---|---|
call | function call `{detail}()` returns a value unknown at build time | for static variants, convert it to `szv()`; for runtime data, use `dynamic()`. |
identifier | identifier `{detail}` could not be resolved to a static value | make it a module-level or function-body `const` with a literal object value; for variant-based styling use `szv()`, for 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, a computed value or `build.importedStaticSz: false` keeps it at runtime; for variant-based styling use `szv()`, for runtime values `dynamic()`. |
member | member expression is not statically resolvable | extract the value to a module-level `const`; for variant-based styling use `szv()`, for runtime values `dynamic()`. |
szv-factory | szv factory `{detail}()` could not be compiled: its config fails at `{path}` | make every variant value at that path a static sz object literal with canonical keys and non-overlapping branches; for runtime data use `dynamic()`. |
other | expression of type `{detail}` cannot be read at build time | use a literal sz object or a module-level `const`; for variant-based styling use `szv()`, for runtime values `dynamic()`. |
variant | key `{detail}` holds a value only known at runtime and no sz property claims it, so its classes were never collected | if the key is a variant, give it a static sz object literal or a two-branch `c ? { … } : { … }`; if it is an sz property, check its spelling; for runtime values use `dynamic()`. |
For variant, {detail} is the key path from the top of the object
(md.hover), and the position is the key’s. The rest of the object still
compiles: sz={{ m: 1, hover: x }} becomes
className={_szcn("m-1", _szPart({ hover: x }))}, so x is styled at runtime,
but nothing told Tailwind to generate its classes. A three-arm chain, a
conditional with one empty branch, and a namespace import under a variant take
the same path; c ? { … } : { … } with two literal branches compiles. When a
css variable or another conditional sits beside the key, the whole object goes
to _sz(…) instead, as an unresolvable spread does. A key no sz table claims
takes the same path, since it may be a variant the project declares
(@custom-variant, --breakpoint-*); a misspelt property key is therefore
reported here too.
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-spread: This `sz` object spreads a value the build cannot read, so the object falls back to the runtime. — help: use the array form: `sz={[x, { … }]}` in place of `sz={{ ...x, … }}`. | missing output | Use the array form. |
[csszyx] fallback-nudge: This `sz` array element is an object literal with a runtime value, so the whole element is resolved at runtime. — help: use finite literal ternary branches where you can, or move the runtime values to `dynamic()`. — note: its classes are still collected, best-effort. | nudge | Optional. Prefer literal ternary branches. |
[csszyx] style-override: This element spreads props that may carry `style`, and the `style` that `sz` writes can replace it. — help: move the spread `style` to an explicit `style` prop, so csszyx merges both values. | nudge | Pass style explicitly so both values merge. |
[csszyx] sz-recover: `szRecover` takes only a string literal. — help: write `szRecover="csr"` or `szRecover="dev-only"`. — note: no `data-sz-recovery-token` is written for this element. | nudge | Use a literal. |
[csszyx] sz-recover: `szRecover` has an unknown mode `"{mode}"`. — help: use `"csr"` or `"dev-only"`. — note: no `data-sz-recovery-token` is written for this element. | nudge | Use "csr" or "dev-only". |
[csszyx] class-precedence: `sz` wins over the runtime `className` on this element in any attribute order, so a caller's `className` cannot override these styles. — help: 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] spread-split-class: `<{elementName}>` has class names or `sz` on more than one side of a spread; in React, Preact, Solid and Qwik the last class attribute replaces the others, so {earlier} will not apply there. — help: write them in one `sz` array after {spreadPlace}, in the order that should win: `sz={[{example}]}`. — note: in Vue JSX every class is merged, so every side applies there. | missing output | {earlier} names what the sides before the last hold: a class name’s value, or the classes of an sz. {example} is built from the element: where to put the array (after the spread, named when it is a plain name) and what goes in it — its class values, the spread’s className or class as you wrote it, and your sz objects, such as after `{...rest}`, in the order that should win: `sz={['card', rest.className, { … }]}` . A spread may carry a class name of its own, so csszyx merges class names and sz only on the same side of one; each side becomes one class name where its sz stood, and the platform decides between them. Write everything in one sz array after the spread, in the order that should win. Vue JSX merges every class instead, so there all apply. In a Svelte component the two class attributes stop the build with Svelte’s attribute_duplicate: Attributes need to be unique; the Svelte adapter compiles only an sz object, so there write {...rest} class="card {rest.class ?? ''}" sz={{ p: 4 }} — class and sz on the same side after the spread. |
[csszyx] duplicate-sz: `<{elementName}>` carries {count} `sz` attributes, merged in order as `sz={[first, …, last]}`: the later one wins for each property. — help: 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. |
[csszyx] merge-classifier-unavailable: The merge table is in format {tableFormat}, and this csszyx version reads format {mergeTableFormat}, so the build removes no class that a later sz key covers. — help: install the same csszyx version of every `@csszyx` package, then rebuild; under Turbopack, delete `.csszyx/merge-table.json` and run `csszyx next prebuild`. — note: every class is kept, as before 0.18, so `{ pb: 2, p: 4 }` emits both and the stylesheet order decides. | correctness | The plugin and the engine come from different csszyx releases, or .csszyx/merge-table.json was truncated or hand-edited. Align the @csszyx/* versions; under Turbopack, write the table again with csszyx next prebuild. |
correctness is a third class beside the two above: every class is present, but
they are not the ones a correct build emits. Like missing output it prints in
every mode except quiet: true.
{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 info
finding, not only the fallbacks. That includes a finding your config lowered to
info, such as dead-class, whose styles are missing, so read the count as
“not listed”, not as “handled”:
[csszyx] diagnostics-held: This build held back {count} finding(s) at `info` and listed none of them. help: run a development build with `quiet` unset to see each one with its file and position, or `csszyx check` to list the sz findings among them. note: they are every finding at `info`: by default the ones csszyx handles itself, such as an `sz` prop that falls back to the runtime or a class an `sz` class on the same element covers, and any finding `csszyx.config` sets to `info`. docs: https://csszyx.com/e/diagnostics-heldRun 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 the ones at error, or
at the level --fail-on names. 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.
Each line is headed by its code, and the rows below quote the runtime’s form:
with no location, the sentence follows the code on the first line. A build
engine that knew the location names it after the code, as the engine’s own
messages do, and puts the sentence on the next line.
In production the runtime prints the code, its args as JSON and the code’s page
instead of the sentence: [csszyx] removed-key: {"form":"removed","key":"maskFrom"} https://csszyx.com/e/removed-key.
Conditional objects
Section titled “Conditional objects”Key and value diagnostics also inspect statically resolved conditional branches,
including ternary spreads, whole-object ternaries, a ternary on one key
(touch: m ? 'pan-x' : 'auto', checked for a known key only) 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 reports unresolvable-spread in place of the fallback
nudge, since both name the same site. 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-key: `{key}` is not an sz property. — help: write `{suggestion}` if you meant it; for a custom utility, define it with Tailwind's `@utility`. — note: the class is still emitted, and it styles nothing unless Tailwind serves that utility. | The class is in the DOM — grep for it. With no known key one edit away, the help starts check the spelling. |
[csszyx] removed-key: `{key}` was removed, and nothing is emitted for it. — help: {note}. | The removed key emits no class. Apply the replacement shape from the note. every mode. |
[csszyx] removed-key: `{key}` is an alias of `{canonical}`, and nothing is emitted for its runtime value. — help: write the values {canonicalAllowed} on `{canonical}`, and any other value on its own key: {groups}. | Build only. An alias of a stand-alone key (touchAction: c) whose value the build cannot read: some values stand alone, others moved to group keys. A literal branch that moved is named as a moved value instead, and an alias whose every literal stands alone gets the canonical-key row. |
[csszyx] canonical-key: `{key}` is an alias of `{canonical}`, and nothing is emitted for it. — help: write `{canonical}`. | The alias emits no class; rename it to the canonical key. |
[csszyx] numeric-key: `sz` received a numeric key `{key}`, and its value is ignored. — help: pass an object of sz keys here; a numeric key usually means an array or a spread was passed in its place. | You probably passed an array to sz={{ … }}. |
[csszyx] removed-key: The boolean shorthand `{rawKey}` was removed, and nothing is emitted for it. — help: write `{ {key}: '{value}' }` instead, or run `csszyx migrate`. | every mode. 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. |
[csszyx] removed-key: `{rawKey}` was replaced, and nothing is emitted for it. — help: write `{ {key}: '{value}' }` instead, or run `csszyx migrate`. | A font-variant-numeric flag that was canonical until 0.18.0 (tabularNums, liningNums, …). every mode, like the row above. |
[csszyx] removed-key: `{rawKey}` was replaced, and nothing is emitted for it. — help: write `{ {key}: true }` instead, or run `csszyx migrate`. | ordinal and slashedZero, now numOrdinal and numSlashedZero. every mode, like the rows above. |
When a key warning has no source location, csszyx names the object and the caller in its place, after the code, so you can find it:
[csszyx] unknown-key: {place} `bgg` is not an sz property.where {place} is the object’s shape and the first frame of your code:
sz object was {"p":4,"bgg":"red-500"} · from at Card (/src/Card.tsx:8:5)And once per process, after the first one, a pointer at the whole-project scan:
[csszyx] dev-warnings-partial: The `sz` key warning above has no source location, so it does not name the file that triggered it. help: run `npx @csszyx/cli check` to list every file and key at once; set `CSSZYX_NO_PROJECT_SCAN_HINT=1` to hide this line.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.
The unknown-key and canonical-key reports print once per message per process, so server-side rendering does not repeat them on every request. After 512 distinct keys they stop, once, with:
[csszyx] diagnostics-capped: This process has reported {count} distinct unknown sz keys, so further ones are suppressed. help: look above for one key repeating with different names; sz keys built from data are the usual cause.An object whose keys come from data — one per record — would otherwise make every render a new report. The keys printed above the line name the object to look at. Production is unaffected.
Values that generate no CSS
Section titled “Values that generate no CSS”| Message | Notes |
|---|---|
[csszyx] off-scale-value: `{key}: {value}` is not on Tailwind's spacing scale, so the class produces no CSS. — help: use a quarter step such as `1.25`, `1.5` or `1.75`, or a unit value such as `'{value}rem'`. | |
[csszyx] unsigned-value: {file}:{line}:{column}, then `{className}` produces no CSS: this project's Tailwind serves `{utility}` but no negative form of it. — help: drop the sign, or define the negative with Tailwind's `@utility`; `csszyx check --rule unsigned-value` lists every one. — note: the class is still emitted, and it styles nothing. | Kind unsigned-value of the dead-class report, error under both presets, printed where that report is and never failing the build. The class is written the way Tailwind spells a negative ({ p: -2 } → -p-2, { hover: { p: -2 } } → hover:-p-2), and the project’s own Tailwind is asked whether it serves it: stock Tailwind has no negative padding or width, and a project that defines @utility -p-* serves -p-2 and hears nothing. Said only when the class without the sign is served (hover:p-2 for hover:-p-2): { p: '-medium' } → -p-medium is dead unsigned too, and is a plain dead-class. Said once, instead of a dead-class line. A length keeps its sign inside the bracket ({ mt: '-10px' } → mt-[-10px]) and is not this finding. |
[csszyx] unserved-variant: `{className}` produces no CSS: Tailwind serves no `{variant}` variant. — help: write `{ '[{variant}]': { … } }` to match the HTML attribute, or `aria: { {variant}: { … } }` for the ARIA state. | Kind unserved-variant of the dead-class report, error under both presets, printed where that report is and never failing the build. A key holding an object is a variant ({ hidden: { p: 4 } } → hidden:p-4), and Tailwind has no bare hidden, readonly or expanded variant. The build and csszyx check ask the project’s own Tailwind, and say it when the class with exactly one variant dropped is served (p-4 for hidden:p-4), once, instead of a dead-class line. The help follows the name: the attribute in brackets and the state under aria for an ARIA state that is an HTML attribute too (hidden, readonly, selected), aria alone for any other ARIA state (write `aria: { {variant}: { … } }` for the ARIA state.), and check the spelling of `{variant}`. for any other name. An HTML attribute that is no ARIA state (inert, open, default) is served bare by every Tailwind v4, so it is not one of these names. Every form ends with note: the class is still emitted, and it styles nothing unless the project defines `{variant}` with `@custom-variant`.: a project that does serves the class and hears nothing. In development the runtime says it too, on Node and SSR, for an ARIA state that the Tailwind csszyx is built with serves no bare variant for (hidden, readonly, selected, expanded, pressed, busy, current, live, atomic, modal on 4.3.3), once per name, and never for another name, which only the project’s stylesheet can define. |
[csszyx] property-object: `{key}` sets a property and is not a variant, so its object `{ {nestedKeys} }` emits `{key}:*` classes that produce no CSS. — help: move the nested keys up a level; for colour opacity, write `{ color: '…', op: … }`. | The classic p: { bg: … } mistake. |
[csszyx] closed-enum-value: `{key}: '{value}'` is a CSS value, and `{key}-{value}` produces no CSS. — help: use the short form: `{ {key}: '{suggestion}' }`. | browser too. flex-start → start, space-between → between, … |
[csszyx] broken-opacity: `{className}` carries an opacity modifier that does not apply: its theme token resolves to the bare comma triplet `{value}`, which `color-mix()` cannot dim. — help: wrap the token's variable in a colour function, such as `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] numeric-font-weight: `weight: '{value}'` is a numeric string, so `font-{value}` produces no CSS. — help: write `weight: {value}` as a number; it emits `font-[{value}]`. | A weight written as a string. The NUMBER form brackets for this reason. |
[csszyx] closed-enum-value: `{key}: '{value}'` has no Tailwind class, and nothing is emitted for it. — help: use `'grayscale'` or `'subpixel'`. | {key} is fontSmoothing. |
[csszyx] closed-enum-value: `{key}: '{value}'` has no Tailwind class, and nothing is emitted for it. — help: use `'italic'` or `'normal'`; for any other style, such as oblique, write `css: { fontStyle: '{value}' }`. | {key} is fontStyle. |
[csszyx] closed-enum-value: `{key}: '{value}'` is not a `{key}` value. — help: use one of: {values}. — note: the class `{class}` is still emitted, and it styles nothing unless a rule of your own matches it. | display, position, visibility, isolation and the num*, touch* and contain* keys. See below. |
[csszyx] moved-value: `{key}: {value}` moved to `{newKey}`, and nothing is emitted for it. — help: write `{ {newKey}: {newValue} }`, or run `csszyx migrate`. | A touch value that became its group’s own key (touch: 'pan-x' is touchPanX: 'x'), also when written on the old alias (touchAction: 'pan-x'). every mode. The class is not emitted. The build names it wherever it is written: in a ternary, under any variant, group, peer, data, aria, has and not included, and in a conditional object branch. |
[csszyx] closed-enum-value: `{key}: {value}` is not a `{key}` value; its class `{class}` belongs to `{ {groupKey}: {groupValue} }`. — help: write `{ {groupKey}: {groupValue} }`, so it combines with the other groups instead of resetting them. — note: `{key}` takes one of: {values}. | A stand-alone key holding a value of one of its groups (contain: 'paint', nums: 'tabular'). The class it names styles, but as a stand-alone value it resets the group keys written before it. See one key per group. |
[csszyx] global-before-group: `{key}: {value}` comes before `{group}` in one sz object, so `{group}` replaces it and the `{key}` value styles nothing. — help: to override the group, layer it: `sz={[base, { {key}: '{value}' }]}`. — note: a spread such as `{ ...base, {key}: '{value}' }` keeps the same key order, so it does not fix this. | every mode. See one key per group. |
[csszyx] runtime-family-conflict: `{key}` takes a runtime value beside `{other}` in one sz object, so both classes can be emitted and Tailwind's stylesheet order decides which applies, whatever the object's order. — help: layer them, later wins: `sz={[{ {key}: … }, { {other}: … }]}`. | Build only. See one key per group. |
[csszyx] non-variant-object: `{key}` is not a variant, so its object emits `{key}:` classes that produce no CSS. — help: a `--*` key takes a declaration value; `container` takes `true`. | The class is emitted. Only --* and container are reported — see below. |
[csszyx] runtime-value: `{key}` cannot take a runtime value: Tailwind has no utility for this key that reads a CSS variable. — help: name the values with `szv()`, or use `dynamic()` for open-ended data. — note: no class or variable is emitted for `{key}`. | Build only — a literal on the same key is unaffected. See below. |
[csszyx] per-side-border-style: `{key}: '{value}'` emits nothing: Tailwind has no per-side border style. — help: use `borderStyle: '{value}'` for every side, or a number on `{key}` for the width. | A style keyword on a side key. See below. |
Closed keys name a value outside their set
Section titled “Closed keys name a value outside their set”display, position and visibility 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.
isolation and the grouped keys (nums, numFigure, numSpacing,
numFraction, touch, touchPanX, touchPanY, contain, containSize) keep
their utility’s fixed part on a typo instead:
{ numSpacing: 'tabulr' } emits tabulr-nums, which cannot land on a class
name of your own.
CSS closes every one of these 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 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' }.
One key per group
Section titled “One key per group”font-variant-numeric, touch-action and contain take one key per group of
their CSS grammar (numSpacing, touchPanX, containPaint, …) and one key for
the keywords that stand alone (nums, touch, contain). In one object the
later key wins: the stand-alone key resets the group keys written before it,
and the group keys written after it replace it. Across the layers of an sz array
or an szv selection the later layer wins the same way.
A stand-alone key written before a group key of its family is therefore dead, and csszyx names it. Written by hand it is a leftover; it is also exactly the shape a spread override leaves, because a spread keeps the overridden key at the place the spread gave it:
const base = { contain: 'strict', containPaint: true };<div sz={{ ...base, contain: 'none' }} /> // ⚠️ contain-paint: { contain: 'none', containPaint: true }<div sz={[base, { contain: 'none' }]} /> // ✅ contain-none: the later layer winsA runtime value on one side of a family beside the other side cannot be settled by the object’s order at build time: the build emits both classes, and Tailwind’s stylesheet puts the stand-alone class last whatever the object says. The build names it; write the two sides as layers instead:
<div sz={{ touch: 'none', touchPanX: pan ? 'x' : undefined }} /> // ⚠️ named at build<div sz={[{ touch: 'none' }, pan && { touchPanX: 'x' }]} /> // ✅ later winsA stand-alone key holding a value of one of its groups (contain: 'paint')
emits that group’s class, but as a stand-alone value it resets the group keys
written before it, so csszyx names the group key to write instead.
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] runtime-value: `{key}` cannot take a runtime value: Tailwind has no utility for this key that reads a CSS variable.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 listPos numFigure numFraction nums numSpacing 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”A colour key takes its opacity in the object form, so a string with a slash
opacity (bg: 'red-500/50') emits nothing on every lane. The build reports it
with the kind slash-opacity-string (an error under recommended), and the
runtime prints the same text once per value, in the browser and in production
too, because the class is missing. A trailing ! is left out of the value it
names.
The object form is the one spelling for an opacity. A plain slash opacity
inside it (bg: { color: 'red-500/50' }) is still written as the class, and the
build reports it with the kind color-slash-opacity (an error under
recommended) so the opacity moves to op. When both are given, op is the
opacity: { color: 'red-500/50', op: 20 } writes bg-red-500/20. A trailing
! ends the class (bg-red-500/20!). Anything else after the slash
(red-500/50/30) is written as given and judged at build.
Any other colour string is written as given, on every lane: bg: 'white/50'
is bg-white/50, bg: '50%' is bg-[50%]. Whether the project’s Tailwind
serves the class is its answer at build, where the dead-class check reports a
class that produces no CSS; the runtime does not judge it.
| Message | Notes |
|---|---|
[csszyx] slash-opacity-string: `{key}: '{color}/{opacity}'` emits nothing: a string value cannot carry a slash opacity. — help: use the object form: `{ color: '{color}', op: {opacity} }`. | build, browser too, production too. |
[csszyx] color-slash-opacity: `{key}: { color: '{base}/{opacity}', op: {op} }` sets opacity twice; a colour object takes its opacity in `op`, not in `color`. — help: write `{ color: '{base}', op: {op} }`. — note: the class uses `op: {op}`; the `/{opacity}` in `color` is ignored. Without op: [csszyx] color-slash-opacity: `{key}: { color: '{base}/{opacity}' }` puts the opacity in `color`; a colour object takes it in `op`. — help: write `{ color: '{base}', op: {opacity} }`. — note: the class still applies opacity {opacity}. | build (color-slash-opacity), runtime in development. The class is still written; with both, op is the opacity. An op that is not a number or a string is named as if absent. |
Mask layers
Section titled “Mask layers”| Message | Notes |
|---|---|
[csszyx] unknown-field: `{owner}` has no field `{member}`, and nothing is emitted for it. — help: `{owner}` takes `{ {allowed} }`. | browser too. Catches maskLinear: { form: … } for from. |
[csszyx] moved-value: `mask: '{value}'` names a gradient layer, which moved to `{key}`, and nothing is emitted for it. — help: write this layer on `{key}`; `mask` takes only `none`, a `url()` or a CSS variable. | build, browser too, production too, with the kind moved-value: nothing is emitted for the value. A trailing ! is left out of it. 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. {got} is null, an array, or the value’s typeof.
| Message |
|---|
[csszyx] szv-config-invalid: The `szv` config is not an object (got {got}), so `szv` returns an empty sz object. — help: pass an object such as `{ base: { … }, variants: { … } }`. |
[csszyx] szv-config-invalid: `base` in the `szv` config is not an sz object (got {got}), so `szv` ignores it. — help: make `base` an sz object, such as `{ p: 4 }`. |
[csszyx] szv-config-invalid: `variants` in the `szv` config is not an object (got {got}), so `szv` returns only `base`. — help: make `variants` an object of dimensions, such as `{ size: { sm: { p: 2 } } }`. |
[csszyx] szv-config-invalid: `defaultVariants` in the `szv` config is not an object (got {got}), so no variant gets a default value. — help: make `defaultVariants` an object of values, such as `{ size: 'sm' }`. |
[csszyx] szv-config-invalid: `variants.{dimension}` is not an object of values (got {got}), so it adds no classes. — help: make `variants.{dimension}` an object that maps each value to an sz object. |
[csszyx] szv-config-invalid: `variants.{dimension}.{token}` is not an sz object (got {got}), so it is ignored. — help: make it an sz object, such as `{ p: 4 }`, or `null` to add nothing. |
[csszyx] sz-object-depth: `{where}` in the `szv` config nests deeper than {maxDepth} levels, so `szv` rejects it at render. — help: flatten `{where}`; nesting this deep usually means the config was built from untrusted or looping data. |
[csszyx] szv-forbidden-key: `{where}` in the `szv` config has the forbidden key `{key}`, so it is ignored. — help: remove the key; csszyx never reads `__proto__`, `constructor` or `prototype`. |
[csszyx] szv-unknown-variant: The `szv` selection names the variant `{key}`, which `variants` does not declare, so it is ignored. — help: declare `variants.{key}`, or remove `{key}` from the selection. |
[csszyx] szv-unknown-variant-value: The `szv` selection sets `{key}` to `{value}`, which `variants.{key}` does not define, so it adds no class. — help: add `{value}` to `variants.{key}`, or use one of `{allowed}`., or add `{value}` to `variants.{key}`, which defines no value yet. when variants.{key} defines no value |
The last two also fire from the precompiled fast path, with identical wording.
If you see [object Object] in class:
[csszyx] szv-object-as-string: An sz object from `szv()` was used as a class string, such as `className={someSzv({ … })}`, which renders `[object Object]` in a production build. help: pass it to an `sz` prop, or turn it into a class string with `szr(…)` first. note: a development build renders an empty string in its place.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] lite-sz-object: `{fnName}()` takes class strings and received the plain object `{received}`, because the compiler could not resolve this `sz` prop at build time. — help: write `sz={[cond ? varA : varB, { key: 'val' }]}`; a spread of a conditional, such as `sz={{ ...(cond ? varA : varB), key: 'val' }}`, is the usual cause. | A spread the compiler could not fold reached the lite runtime. Development only: a production build skips the check. |
Thrown. [csszyx] lowering-not-loaded: A string helper received an sz object, but the module that turns sz objects into class names is not loaded, so it cannot produce class names for it. — help: add `import '@csszyx/runtime/lowering';` once at startup when the code runs outside the csszyx bundler plugin, such as in unit tests or scripts; the plugin loads it for files that can pass objects at runtime. | Running outside the bundler plugin — unit tests, scripts. |
Thrown. [csszyx] runtime-prefix-conflict: Two builds sharing one JavaScript runtime registered the Tailwind prefixes `{first}` and `{second}`, so csszyx cannot tell which prefix their classes take. — help: keep independently configured applications on separate `@csszyx/runtime` instances, or align their Tailwind prefix. | Two independently built applications share a runtime realm but their stylesheets use different prefixes. csszyx stops before runtime lowering can emit a class owned by the wrong stylesheet. When one of them has no prefix, the sentence reads registered different Tailwind prefixes, `{first}` and none. |
Each of these is thrown as an error whose code is the id after [csszyx]
and whose args hold the values the sentence names; a production build
throws the code, its args as JSON and the code’s page instead of the
sentence. Test it with isCsszyxError(err) and err.code, from csszyx or
@csszyx/runtime.
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 on the CSS your Tailwind compiles, so it reads every class for
what it sets. A theme token can still make a class mean two things at once,
and the theme group registry — which classify reads — says so once.
| Message |
|---|
[csszyx] theme-collision: Theme token `{name}` shadows a built-in utility keyword, so Tailwind compiles both meanings into one class, which sets the token's property and the built-in's. — help: rename the token; while it shares a built-in name, no class order or `szcn` call can fix this. — note: a later class that sets only one of them does not replace it in `szcn`, so both apply and stylesheet order decides which wins; `classify` reports the built-in meaning only. |
[csszyx] theme-collision: Theme token `{name}` shadows a built-in value, so Tailwind compiles both meanings into one class, which sets the token's property and the built-in's. — help and note as above. A colour token shadows a utility keyword; a text-size, font-family or font-weight token a value. |
[csszyx] theme-token-ambiguous: Theme token `{name}` is defined as both a colour and a text size, so `classify` cannot name the property of `text-{name}`, and Tailwind compiles that class for only one of them. — help: rename one of the tokens. — note: a `text:color` or `text:size` placement in `splitBox` does not match `text-{name}`; `szcn` merges on the compiled CSS and is not affected. |
[csszyx] theme-token-ambiguous: Theme token `{name}` is defined as both a font family and a font weight, so `classify` cannot name the property of `font-{name}`, and Tailwind compiles that class for only one of them. — help as above — note: a `font:family` or `font:weight` placement in `splitBox` does not match `font-{name}`; `szcn` merges on the compiled CSS and is not affected. |
[csszyx] register-szcn-groups-ignored: `registerSzcnGroups` no longer changes what `szcn` merges: `szcn` merges on the CSS your Tailwind compiles, and the groups you register only name the property that `classify` and `splitBox` read. — help: declare the tokens under `@theme` (`--color-*`, `--text-*`, `--font-*`, `--font-weight-*`), delete the CSS rules you wrote for those classes, and use a csszyx integration that supplies a merge table. — note: `szcn` never removes a class from `@utility`, a plugin or plain CSS; without a table it removes only exact repeats. |
One more is not about a token at all. It is printed once, in development, the
first time szcn meets two classes with no merge table registered:
| Message |
|---|
[csszyx] merge-table-missing: `szcn` has no merge table in this run, so it removes only exact repeats: `szcn('p-2', 'p-4')` keeps both classes. — help: use the csszyx plugin for Vite, Rollup, webpack or Next.js, which registers one; for jest, run the build or `csszyx next prebuild` first. — note: the csszyx plugin registers no table under esbuild, Rspack, Rsbuild, Rolldown, Farm or Bun. |
A table the build wrote in a format this runtime does not read is refused the
same way, which is what a stale .csszyx/merge-registration.* from another
csszyx release looks like to a jest run. Rejection clears the previous table
and cached merges. The regeneration warning appears once per session in both
development and production (there as [csszyx] merge-table-format-skew: {"format":2,"expected":1} https://csszyx.com/e/merge-table-format-skew):
| Message |
|---|
[csszyx] merge-table-format-skew: The merge table was written in format {format}, and this csszyx version reads format {expected}, so `szcn` ignores the table and removes only exact repeats. — help: run the build, or `csszyx next prebuild` on Next.js, with the installed csszyx to write it again. |
See Where the table comes from.
The registerSzcnGroups message is printed once, in development, on the first
hand-written call to registerSzcnGroups. On 0.17 that call was how a class
written in plain CSS joined a merge group. Every group it took is a @theme
namespace, so the migration is to declare those tokens under @theme and
delete the rules you wrote for the classes, with an integration that supplies
the merge table. A class your own CSS selects on stays out of every merge, and
so does one from @utility, a plugin or plain CSS. Without that table, szcn removes only
exact repeats.
The fix in the three theme-token cases is renaming the token in your @theme block. With
--color-balance declared, szcn('text-red-500', 'text-balance') is
'text-balance' — that class now sets color too — while
szcn('text-balance', 'text-red-500') keeps both, because the colour class does
not cover text-wrap.
dynamic()
Section titled “dynamic()”| Message | Production? |
|---|---|
[csszyx] dynamic-unresolvable: The runtime value of `{key}` has type `{type}`, which cannot be resolved to CSS, so the property is omitted. — help: pass a number, a token or a CSS length; for a responsive object, use `dynamic()`. | Yes — this one is deliberately kept in production, because the property is dropped. |
[csszyx] dynamic-non-boolean: The runtime value of `{key}` has type `{type}`, and only a boolean toggles this class, so it is omitted. — help: pass `true` or `false`; write other values as literals, or use `dynamic()` for a responsive object. | 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] dynamic-unsafe-arbitrary: The arbitrary value in `{utility}` could inject CSS, so `dynamic()` omits the class. — help: keep untrusted data out of arbitrary values, or validate it before it reaches `dynamic()`. — note: only the first dropped value of a session is reported. | No — dev only. See Security. |
[csszyx] dynamic-injection-growth: `dynamic()` has injected {count} or more unique classes this session, and injected CSS rules are never removed, so the CSSOM keeps growing and style recalculation slows. — help: drive continuously changing values, such as a width that changes every animation frame, through a CSS variable instead. | No — dev only, once. |
Thrown and caught. [csszyx] dynamic-manifest-unreachable: The `dynamic()` manifest could not be fetched (HTTP status {status}), so every class is treated as new and injected at runtime. — help: serve the manifest at the URL `setManifestUrl` names, or turn `build.emitManifest` on. | Never printed: dynamic() catches it and injects every class. |
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}`, - `{key}` is read from `{movedTo}`, not from the plugin options (for diagnostics), 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] config-invalid: {file}, then This file has {count} problem(s); each one is ignored, and the rest of the file applies., or This file has {count} problem(s), so it is not applied and the `recommended` levels apply. when it did not load, exported nothing to read, or holds no diagnostics beside a key that belongs there, then one line per problem — - ({severity}) {path}: {text}, or - ({severity}) {text} for the file as a whole, where {severity} is error or warning, with the problem’s own help: and note: lines under it. Each problem is worded by its own id, listed in the rows below: an id no rule has (unknown-rule-id); an id whose level the file cannot set; an id csszyx no longer reports; a level (config-level-invalid), a preset (config-preset-unknown) or a shape (config-field-invalid) the file cannot use; a key csszyx does not read (config-field-unknown); an override glob that matches no file (config-glob-unmatchable); a second config file (config-file-duplicate); a file that did not load (config-unloadable) or exports nothing (config-no-default-export) — and last — help: https://csszyx.com/docs/reference/config/#csszyxconfig describes every field; a dev server, a watch build and `csszyx next watch` read it again when it or a module it imports is saved. | Warning, once per build; csszyx check prints it too and exits 1 unless every problem is a plugin option in the file, an id csszyx no longer reports, or the old csszyx init template below. csszyx.config explains each field. |
[csszyx] project-reload: {file}, then This file changed, so csszyx read its levels again, recompiled every module and reloaded the page. | Printed by a Vite dev server, once per save, creation or deletion of csszyx.config or of a module it imports ({file} names the file saved, from the project root), whatever quiet says. The server read the file again, printed its problems if it has any, and compiled every module so each finding is listed again at the level the file now gives it. Nothing to do. |
This file could not be loaded, so the `recommended` levels apply. — help: fix the error the loader reported. — note: the loader reported: {reason}. | One line of the warning above, config-unloadable, for the file as a whole. The config file threw, or has a syntax error. {reason} is the first line of the error, ending in (at {file}:{line}:{column}) when the error names a place in the file. |
This file holds TypeScript syntax under a JavaScript name, so it could not be loaded and the `recommended` levels apply. — help: rename it to `{renameTo}`. — note: the loader reported: {reason}. | In place of the line above when a .js or .mjs config fails on a syntax error and holds a type annotation, a type import or another TypeScript-only form. {renameTo} is csszyx.config.ts in a "type": "module" package and csszyx.config.mts elsewhere. |
{n} config error(s) in {file}, which fail the run. | The last line of csszyx check, marked ✖, when the warning above has an error problem; on stderr under --json. The run exits 1 whatever the findings. |
This file has no default export, so nothing in it applies. — help: write `export default defineConfig({ … })`, importing `defineConfig` from `csszyx`. | One line of the warning above, config-no-default-export, for the file as a whole: it loaded and exported nothing to read, such as export const config = defineConfig({ … }). csszyx check exits 1. |
default export: `{key}` is not a field csszyx reads, so nothing under it applies. — help: rename it to `{suggestion}`. | One line of the warning above, config-field-unknown: a top-level key one or two letters from diagnostics, which {suggestion} names. Every level under it would fall back to the defaults, so csszyx check exits 1. |
{key}: `{key}` is a plugin option, and this file does not set plugin options, so it is ignored. — help: pass it to the csszyx plugin in your bundler config. | One line of the warning above, config-field-unknown: any other top-level key, such as a plugin option an older csszyx init scaffolded into the file. A warning; csszyx check does not exit 1 for it. |
`diagnostics.{key}` is not a field csszyx reads, so it sets nothing. — help: use `preset`, `rules`, `allow` or `overrides`. · `diagnostics.allow.{key}` is not a field csszyx reads, so it allows nothing. — help: use `classes` or `tokens`. | Lines of the warning above, config-field-unknown: a key diagnostics or allow does not take. csszyx check exits 1. |
diagnostics.rules: No rule or diagnostic is named `{ids}`, so a level set under that name applies to nothing. — help: write `{suggestion}` if you meant it; https://csszyx.com/docs/reference/warnings/ lists every id. or check the spelling; … | One line of the warning above, unknown-rule-id, under diagnostics.rules or diagnostics.overrides[{i}].rules, for an id that names nothing; {suggestion} is the id it most likely means, when one is near. The entry is dropped; csszyx check exits 1. |
diagnostics.rules: `{id}` is set to `{level}`, which is not a level, so that setting is ignored. — help: use `'off'`, `'info'`, `'warn'` or `'error'`. · diagnostics.preset: `{preset}` is not a preset, so the `recommended` levels apply. — help: use `'recommended'` or `'atomic'`. | Lines of the warning above, config-level-invalid and config-preset-unknown; {level} and {preset} are the values as JSON writes them, such as "fatal". csszyx check exits 1. |
{field}: `{field}` is not an object, so it is ignored., or the same line for a list of names, a list of overrides, a { files, rules } object, a glob list or an object of id and level, each with a help line that shows the shape to write | Lines of the warning above, config-field-invalid: a section of the file whose shape csszyx cannot read. For the default export itself the line reads The default export is not an object, so nothing in this file applies.. csszyx check exits 1. |
{file}: `{read}` is read first, so this file is not read and nothing in it applies. — help: keep one config file: move what this file sets into `{read}`, then delete this file. | One line of the warning above, config-file-duplicate, for each other csszyx.config file beside the one read. A warning. |
diagnostics.rules: `{id}` is printed to the console or thrown, so `csszyx.config` sets no level for it. — help: remove it; `rules` sets the level of engine diagnostics and `csszyx check` findings only. | One line of the warning above, config-id-not-settable, under diagnostics.rules or diagnostics.overrides[{i}].rules: a real id, of a line the runtime prints or an error a call throws, which no level reaches. rules sets the engine’s diagnostics and the passes of csszyx check. The entry is dropped; csszyx check exits 1. |
diagnostics.rules: `{id}` is no longer reported, so a `csszyx.config` level or a `--rule` or `--ignore-rule` flag naming it applies to nothing. — help: name `{replacedBy}` instead, which reports what `{id}` did. or remove it. | One line of the warning above, removed-rule-id, under diagnostics.rules or diagnostics.overrides[{i}].rules, when the id is one a release shipped and csszyx no longer reports, such as other ({replacedBy} is sz-diagnostic). The entry is dropped; it is a warning, so csszyx check does not exit 1 for it. csszyx check --rule and --ignore-rule print the same text as its own [csszyx] removed-rule-id: warning and go on, selecting nothing for it. |
default export: `{key}` is not read at the top level, so nothing under it applies. — help: move it under `diagnostics`, as `diagnostics.{key}`. | One line of the warning above, config-field-unknown: rules, preset, allow or overrides written beside diagnostics instead of inside it, as an ESLint flat config has them. csszyx check exits 1. |
`{glob}` is absolute or steps out of the project root with `..`, so it matches no file. — help: write it relative to the project root, such as `src/legacy/**`; csszyx matches each file by its path from there. | One line of the warning above, config-glob-unmatchable: a glob starting with / or a drive letter, or holding a .. segment. A backslash reads as / and ./ is dropped first, so src\legacy matches and ./../src is refused. The glob is dropped; csszyx check exits 1. |
This is the file `csszyx init` 0.17 and earlier wrote, which does not load, so it is ignored. — help: replace it with `export default defineConfig({ diagnostics: { … } })`, importing `defineConfig` from `csszyx`, or delete it. — note: the loader reported: {reason}. | One line of the warning above, config-unloadable, in place of the load failure when the file has the shape of that template, whatever its quotes, spacing, semicolons and comments, and sets no diagnostics: the csszyx.config.js it wrote in TypeScript syntax into a JavaScript project. csszyx check does not exit 1 for it. |
[csszyx] could not write .csszyx/diagnostic-policy.json: {reason} + note: the safelist is written; until the file can be written, the Turbopack loader reports every diagnostic at its default level. | Warning from csszyx next prebuild and csszyx next watch; the command still succeeds. |
[csszyx] policy-state-format-skew: .csszyx/diagnostic-policy.json, then This policy file was written in format {format}, and this csszyx version reads format {expected}, so the levels in it are not used. — help: run `csszyx next prebuild` or `csszyx next watch` with the installed csszyx to write it again. — note: until then csszyx loads the config file itself, in a child process, or applies the `recommended` levels when the project has none. | Warning, once per process, from the Turbopack loader and jest. Another csszyx version wrote the file. |
[csszyx] removed-option:, then `production.mangleMapDelivery` was removed and is ignored: the runtime mangle map is always registered from inside the JS bundle, and the built HTML carries no inline `<script>`. — help: delete the option. — note: a strict `script-src 'self'` policy needs no exception; `window.__csszyx` is opt-in through `production.mangleDebugGlobal`. | Warning, once per build, whenever the removed option is set |
[csszyx] removed-option:, then `production.injectChecksum` was removed and was never read: every production build carries the hydration checksum and the census, whatever it is set to. — help: delete the option. — note: the census lists no classes when `production.mangle` is off, and the browser runs neither the checksum nor the census as script; see https://csszyx.com/docs/security/. | Warning, once per build, whenever the removed option is set |
Thrown. [csszyx] mangle-global-vars-options-invalid:, then The `production.mangleGlobalVars` options break the rules listed below, so the build stops. — help: fix each option the list names. — note: the problems: and one line per problem | 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] mangle-global-vars-options-invalid:, then `production.mangleGlobalVars.enabled` is `true` with no `tokens`, so the build stops. — help: list the custom properties to alias in `tokens`, such as `tokens: ['--brand']`. — note: csszyx aliases only the custom properties listed in `tokens`. | Error |
Thrown. [csszyx] mangle-global-vars-options-invalid:, then `production.mangleGlobalVars.autoPrefix` is set, and choosing tokens by prefix is not available yet, so the build stops. — help: remove `autoPrefix` and list the custom properties in `tokens`. | Error |
Thrown. [csszyx] mangle-global-vars-plan-invalid:, then `production.mangleGlobalVars` cannot alias every listed property safely, so the build stops. — help: fix each usage listed below, or take that property out of `tokens`. — note: the findings: and one line per finding | Error |
Thrown. [csszyx] mangle-map-too-large:, then The CSS variable mangle map is {size} bytes, over its safety cap of {maxBytes} bytes, so the build stops. — help: turn `production.mangleVars` off or split the bundle; to accept this size, raise the `CSSZYX_VAR_MANGLE_MAP_MAX_BYTES` environment variable above {size}. | Error |
[csszyx] compile-sources-unresolved:, then The `compileSources` path(s) `{paths}` do not resolve to a directory from `{root}`, so the build compiles nothing from them and adds none of their classes to the safelist. — help: point each path at a directory, relative to the project root or absolute, such as `compileSources: ['../../packages/vui']`. | Warning, once per root |
[csszyx] transform-cache-disabled:, then The build could not read the installed version of `@csszyx/unplugin` or `@csszyx/compiler`, so the transform cache is off and every build transforms each file again. — help: reinstall `@csszyx/unplugin` and `@csszyx/compiler`, or set `build.cache: false` to hide this line. — note: the build output is the same; only rebuilds are slower. | Ungated, once |
[csszyx] esbuild-mangle-unsupported:, then The esbuild adapter does not support `production.mangle`, so class mangling is off for this build. — help: use the Vite, Rollup or webpack plugin when the build needs mangled class names. — note: emitted JS and CSS keep the original class names, so they still match. | Ungated |
[csszyx] theme-scan-without-typescript:, then `{tsconfig}` does not mention `.csszyx`, so TypeScript does not see the theme types the scan wrote to `.csszyx/theme.d.ts`. — help: add `"{includePath}"` to its `include` array. | Ungated, once per process. {includePath} is .csszyx/theme.d.ts as that config reads it, from its own directory: ./.csszyx/theme.d.ts for a config in the project root. A config a framework writes into .nuxt/, .svelte-kit/ or .astro/ is not named; the root tsconfig.json is. |
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-scan-without-typescript: `tsconfig.app.json` does not mention `.csszyx`, so TypeScript does not see the theme types the scan wrote to `.csszyx/theme.d.ts`. help: add `"./.csszyx/theme.d.ts"` to its `include` array. docs: https://csszyx.com/e/theme-scan-without-typescriptPrinted once per process. Nothing is wrong with your CSS: the scan works, but
no tsconfig TypeScript reads includes the generated .csszyx types, so the
theme tokens it finds never reach editor completion or type-checking. The line
names tsconfig.json, or tsconfig.app.json when the project has only that.
A root tsconfig.json that includes nothing itself and lists references, as
Vite’s template writes it, is read through its references: the line stays
silent when one of them mentions .csszyx, and names the first of them
otherwise. 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-collision: `@utility {name}` is declared twice, so Tailwind merges both declarations into one class, and no single place in your CSS shows what `{name}` sets. help: merge the two blocks into one, or rename one of them. note: every use of `{name}` gets both, including a class an `sz` prop emits, and Tailwind does not warn about it.[csszyx] utility-collision: `@utility {name}` names a class a theme token already generates, so Tailwind merges both into one class, and no single place in your CSS shows what `{name}` sets. help: rename the `@utility` or the theme token. note: every use of `{name}` gets both, including a class an `sz` prop emits, and Tailwind does not warn about it.The first is two @utility blocks claiming one name; the second a @utility
that 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] legacy-source-directive: {stylesheet} `@source "{target}"` names the old safelist, which csszyx no longer writes, so the build stops. help: remove this line; on Next.js, also list `'@csszyx/unplugin/postcss'` before `'@tailwindcss/postcss'` in `postcss.config`. note: csszyx adds the `@source` for `.csszyx/csszyx-classes.txt` itself: the Vite, Rollup and webpack plugins inject it, and on Next.js `@csszyx/unplugin/postcss` does. docs: https://csszyx.com/e/legacy-source-directiveA 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] postcss-plugin-missing: {config} The PostCSS config Next.js reads does not list `@csszyx/unplugin/postcss`, so Tailwind never reads the safelist and no `sz` class produces CSS. help: add `@csszyx/unplugin/postcss` to its plugins, before `@tailwindcss/postcss`. docs: https://csszyx.com/e/postcss-plugin-missingPrinted by csszyx next prebuild and csszyx next watch. Add the plugin line.
[csszyx] postcss-plugin-order: `@csszyx/unplugin/postcss` is listed after `@tailwindcss/postcss`, so Tailwind compiles the stylesheet before csszyx adds the safelist, and the build stops. help: list `'@csszyx/unplugin/postcss'` before `'@tailwindcss/postcss'` in `postcss.config`. docs: https://csszyx.com/e/postcss-plugin-orderA PostCSS error: reorder the two plugins.
The safelist could not be written
Section titled “The safelist could not be written”[csszyx] safelist-unwritable: .csszyx/csszyx-classes.txt The generated safelist could not be written, so the CSS Tailwind generates is missing the classes csszyx found. help: check that the directory exists and is writable. note: the file system reported: {reason}. docs: https://csszyx.com/e/safelist-unwritableThe 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.
Tailwind entries set different prefixes
Section titled “Tailwind entries set different prefixes”[csszyx] prefix-disagreement: The Tailwind stylesheets of this project set different prefixes ({entries}), so no class csszyx emits is served by all of them. help: give every Tailwind stylesheet the same `@import "tailwindcss"` line, or list the stylesheets this build loads with the csszyx `tailwindStylesheet` option. note: no module was transformed, and nothing was written. docs: https://csszyx.com/e/prefix-disagreementA build error; {entries} names each stylesheet with the prefix it sets,
such as src/index.css with prefix(tw). csszyx writes one prefix before every
class, so with two prefixes in play part of the app would get classes that style
nothing. A stylesheet another entry imports has no say of its own, and entries
that differ only in important do not stop the build. Usually one of the listed
files is a fixture or an old copy: name the real one in the tailwindStylesheet
option (configuration). csszyx next prebuild and
csszyx next watch print the same messages naming the --tailwind-stylesheet
flag in place of the option, and the Turbopack loader takes the list as
csszyxTurbopack({ tailwindStylesheet }). When the other entry belongs to
another app under the same root, --ignore <patterns> on those two commands
leaves its directory out for the loader too, and the Jest transformer takes the
same patterns in its ignore option.
[csszyx] ignore-pattern-negated: The ignore pattern `{pattern}` is negated, and an ignore list cannot take one, so csszyx stops. help: narrow the pattern that covers too much, so that nothing has to be taken back out. note: each pattern only adds paths to leave out.An error from csszyx next prebuild, csszyx next watch and the Jest
transformer’s ignore option. Each pattern is matched on its own, so !legacy/keep.css would not take one file
back out of legacy/**: it would match every path except that file, the app’s
real Tailwind entry included.
[csszyx] tailwind-stylesheet-missing: The stylesheet(s) `{paths}` named by the csszyx `tailwindStylesheet` option do not exist under `{rootDir}`, so the build stops. help: list the Tailwind stylesheet this build loads, relative to the project root. docs: https://csszyx.com/e/tailwind-stylesheet-missingA build error. A mistyped path would otherwise leave nothing to read the prefix from. Fix the path.
[csszyx] tailwind-stylesheet-broken: A Tailwind stylesheet did not compile, so csszyx cannot read the Tailwind prefix and transforms no module. help: fix the stylesheet; if this build does not load it, list the stylesheets it does load with the csszyx `tailwindStylesheet` option. note: each failure, as Tailwind reported it: {file}: {reason} docs: https://csszyx.com/e/tailwind-stylesheet-brokenA build error. The stylesheet imports Tailwind, directly or through another
stylesheet, and something it imports could not be loaded, so the prefix it sets
is unknown. {reason} is the first line of what the compile reported, usually a
path that does not resolve. Fix the import. If the file is a leftover the app no
longer loads, name the stylesheets it does load in tailwindStylesheet.
[csszyx] these stylesheets did not compile and never reached Tailwind, so csszyx read the prefix without them: {file}: {reason} help: if the app loads one of them, fix its import; otherwise list the stylesheets the app loads in the csszyx `tailwindStylesheet` option.A warning; the build carries on. A stylesheet that never reaches Tailwind cannot set a prefix, so it does not change what csszyx emits. It is listed because a broken import in a file the app does load is still worth fixing.
[csszyx] {file} imports `{specifier}`, which this build's Tailwind cannot resolve, so csszyx left the stylesheet out of the prefix and the merge model. help: a build that loads one of them stops on that import; import the file by a relative path (only a Vite build follows an alias, through `resolve.alias`). If the build does not load it, list the stylesheets it does load in the csszyx `tailwindStylesheet` option.A warning, printed in production too and muted only by quiet: true, as every
warning about the build’s environment is; once per process for each stylesheet
and import, and the build carries on. The import names neither
a relative nor an absolute path (an alias such as @/styles/theme.css, or a
package), and the resolver of this build’s Tailwind finds no stylesheet for it.
@tailwindcss/postcss, which Next.js, webpack, Turbopack, Rollup and esbuild
builds run, reads such a target as a package and follows no alias and no
tsconfig paths. A Vite build follows resolve.alias, and tsconfig paths
only with resolve.tsconfigPaths and @tailwindcss/vite. If the app loads the
stylesheet, Tailwind stops the build on that import by itself. If it does not
(a leftover copy, a stylesheet of another app), the build is fine, and csszyx
reads no prefix and no theme from that file. csszyx next prebuild and
csszyx next watch print the line naming the --tailwind-stylesheet flag;
they take no quiet option, so they always print it.
[csszyx] merge-origin-unreadable: {file} This Tailwind stylesheet did not compile once csszyx took out its `@utility` blocks and plugin classes, so csszyx cannot tell Tailwind's classes from the project's and merges no class in this build. help: report this csszyx bug at https://github.com/nguyennhutien/csszyx/issues with this message and the plugin or `@utility` block the stylesheet uses. note: every class is kept, so `{ pb: 2, p: 4 }` emits `pb-2 p-4`, a `className` class stays beside the `sz` classes, and `szcn` removes only exact repeats; the compile reported: {reason}. docs: https://csszyx.com/e/merge-origin-unreadableA warning that fires in production, printed once each time the build reads the
stylesheets, and by csszyx next prebuild and next watch. Only Tailwind’s own
utilities merge, so csszyx compiles the stylesheet a second time with its
@utility blocks removed and its plugins registering no class. When that
compile fails, csszyx cannot tell which classes are Tailwind’s, so it merges
none: output is what concatenating the classes gives, and the stylesheet order
decides between them.
The Tailwind prefix changed during a session
Section titled “The Tailwind prefix changed during a session”[csszyx] project-reload: {file} This file changed the Tailwind prefix from `{before}` to `{after}`, so csszyx recompiled every module and reloaded the page. docs: https://csszyx.com/e/project-reloadPrinted by a Vite dev server, once per change, whatever quiet says. Every
module compiled before the edit carried classes under the old prefix, so the
server lowered every source again and reloaded the page. A stylesheet that sets
the first prefix reads This file set the Tailwind prefix `{after}`, so csszyx recompiled every module and reloaded the page., and one that drops it reads
This file removed the Tailwind prefix `{before}`, so csszyx recompiled every module and reloaded the page.. Nothing to do.
[csszyx] project-reload: {file} This file changed which sz keys cover each other, so csszyx recompiled every module and reloaded the page. docs: https://csszyx.com/e/project-reloadThe same reload for a stylesheet edit that changed which class a later sz
key covers — a @theme token added or removed, a rule of your own that starts
or stops selecting on a Tailwind class. Each
module merged its static sz objects under the stylesheet it was compiled
with, so the server compiled every module again. Nothing to do.
[csszyx] merge-covered-held: The build removed {classes} class(es) in {files} file(s), because another class on the same element sets every property they set. help: `csszyx check --rule merge-covered-key --rule merge-covered-class` lists them; `build.mergeCoveredClasses: false` in the plugin options keeps every one. note: this line stays until each id after `--rule` is `'off'` in `diagnostics.rules` of `{configFile}`.{configFile} is the project’s config file, or the name csszyx init would give
it when there is none.
The info rendering of merge-covered-key and merge-covered-class, the
default under the recommended preset. Printed once when a Vite dev server or
a webpack development build starts, and again when an edit to the config during
that session (on a webpack watch, at the next rebuild) gives either id another
level; never in production, and never on Rollup, esbuild, jest or Turbopack. Each class was dropped on purpose: a later key in
the same sz object (merge-covered-key), or an sz class beside a static
className (merge-covered-class), sets every property it sets. The line
names only the ids still at info. If you needed one, the audit shows where it
was; move it into sz after the key that covers it, or set
build.mergeCoveredClasses: false to keep every class. Setting both ids to
off hides the line and changes nothing else.
[csszyx] merge-covered-key: {file}:{line} The build removes `{className}`, written as `{key}`, because a later key in the same sz object sets every property it sets. help: delete `{key}` from the object; to keep every covered class, set `build.mergeCoveredClasses: false` in the plugin options.[csszyx] merge-covered-class: {file}:{line} The build removes `{className}` from `className`, because an `sz` class on the same element sets every property it sets. help: delete `{className}` from `className`; to keep every covered class, set `build.mergeCoveredClasses: false` in the plugin options.The same finding at warn or error — the atomic preset, or a level set in
diagnostics.rules: one line per removed class with its file, line and key,
printed when the dev server or build starts, in production too, and again when
a config edit during a dev server or a webpack watch changes the level.
[csszyx] watch-prefix-changed: `{changed}` changed the Tailwind prefix from `{before}` to `{after}`, and this watch session compiled its modules under the old one, so their classes would style nothing. help: restart the watch session. note: every rebuild fails with this error until the restart, or until the prefix is changed back. docs: https://csszyx.com/e/watch-prefix-changedA watch rebuild error on webpack, rollup -w, vite build --watch, an
esbuild context and rspack. A stylesheet that sets the first prefix reads
`{changed}` set the Tailwind prefix `{after}`, and this watch session compiled its modules without one, so their classes would style nothing.,
and one that drops it reads `{changed}` removed the Tailwind prefix `{before}`, and this watch session compiled its modules under it, so their classes would style nothing.. Those bundlers keep the modules they already
compiled and a plugin cannot make them compile everything again, so every
rebuild fails with this until the prefix is back or the watch restarts.
note: {session} keeps the prefix it last read ({prefix}) until the stylesheets agree again.Closes a prefix disagreement or a
stylesheet that did not compile when either happens during a Vite dev server
(the dev server) or a watch rebuild (this watch session), above its
docs: line. For a disagreement it takes the place of the build’s note, which
says nothing was written: a session that goes on serving has written its
output. It is a warning
there, not an error: the author is mid-edit, and the next save usually fixes it.
note: `csszyx next watch` keeps watching; the loader stops on this error until the stylesheets agree again.Closes the same two messages, above their docs: line and in place of a
disagreement’s build note, when csszyx next watch reads them after a
stylesheet edit. The watch goes on, but the stylesheet facts it records are not
rewritten, so the Turbopack loader stops on the same error until the edit is
fixed.
[csszyx] internal-error: An internal check failed in `createCompilerOptions`, so csszyx stops. help: report this csszyx bug at https://github.com/nguyennhutien/csszyx/issues with this message, the csszyx version and the bundler or command you ran. note: the check that failed: the build asked for the Tailwind prefix before it read the project's stylesheets. docs: https://csszyx.com/e/internal-errorA build error that should never reach you: a bundler lane reached the engine without reading the stylesheets first. Report it with the bundler and its version.
A stylesheet edit changed which sz keys cover each other
Section titled “A stylesheet edit changed which sz keys cover each other”[csszyx] watch-merge-groups-changed: `{changed}` changed which sz keys cover each other, and this watch session merged its modules under the old stylesheet, so an sz object may keep a class a later key now covers, or lack one a later key no longer covers. help: restart the watch session. note: this rebuild finished, and only sz objects that use those keys are stale until the restart. docs: https://csszyx.com/e/watch-merge-groups-changedA later key in a static sz object replaces an earlier one whose CSS it
covers, and the build works that out from your stylesheet when it compiles the
module. A watch rebuild on webpack, rollup -w, vite build --watch, an
esbuild context or rspack keeps the modules it already compiled, so after an
edit such as removing a @theme token those modules still carry the old
answer. The rebuild goes on, since only the objects whose keys changed are
affected; restart the watch to compile them again. A Vite dev server needs no
restart: it compiles every module again and reloads the page.
A rule selects on a class the build already removed
Section titled “A rule selects on a class the build already removed”[csszyx] merge-hook-late: {file} `{className}` was removed from this file before csszyx read a rule that selects on it, so the page would lose that class, and the build stops. help: set `build.mergeCoveredClasses: false` to keep every class, or restart the watch session. note: csszyx reads such rules during the prescan, before it merges; this one came later, from a module the prescan did not reach or from an edit during a watch session. docs: https://csszyx.com/e/merge-hook-lateA build error. A merge never removes a class that a rule selects on: a
rule in a stylesheet, a component’s <style> block, or an arbitrary variant
such as group-[.shadow-md]:p-2, written in markup or lowered from an sz key.
The build reads all of them before its first merge. This stops the build
when it meets one only afterwards, from a module outside the directories it
walks or from an edit during a watch, because the class is already gone from
{file}. A Vite dev server does not stop: it compiles every module again and
reloads the page.
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-turbo-cache-unseeded: The Turbopack production build of `{root}` has no current safelist from `csszyx next prebuild`, so it stops. help: run `csszyx next prebuild` before `next build`, such as `"build": "csszyx next prebuild 'app/**/*.tsx' && next build"` in `package.json`. note: the safelist check reported: {reason}. docs: https://csszyx.com/e/next-turbo-cache-unseededRun the prebuild once, then keep the build script the help names so the next
next build finds the cache already seeded.
[csszyx] next-mangle-vars-unsupported: {file} Turbopack cannot mangle CSS variables in production, and `config.mangleVars` is on, so the build stops at this module. help: set `config: { mangleVars: false }` in `csszyxTurbopack`, or build with webpack (`next build --webpack`), which mangles them. docs: https://csszyx.com/e/next-mangle-vars-unsupported[csszyx] next-mangle-vars-unsupported: The Next prebuild cannot mangle CSS variables in production, and `config.mangleVars` is on, so it stops. help: pass `config: { mangleVars: false }` to `runNextPrebuild`, or build with webpack (`next build --webpack`), which mangles them. docs: https://csszyx.com/e/next-mangle-vars-unsupportedMangling 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-sz-untransformed: {file} This module still holds `sz` syntax after the csszyx transform, so the build stops. help: fix the warning reported for this file, such as a syntax error or an `ast-budget` warning for a file too large to transform; if `sz` here is not the `sz` prop, such as an object key of your own, rename it. docs: https://csszyx.com/e/next-sz-untransformedThe 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 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.
A shard another csszyx version wrote is left out of the safelist, and said:
| Message | Kind |
|---|---|
[csszyx] safelist-shard-format-skew: {shard} + This safelist shard was written in format {n}, and this csszyx version reads format {m}, so its classes are left out of the safelist. + help: run `csszyx next prebuild` with the installed csszyx to write every shard again. | Warning, once per shard, from the loader, csszyx next prebuild and csszyx next watch. |
Each source’s classes sit in a shard under
.csszyx/cache/safelist-shards/, and this one was written by a csszyx whose
shards this one cannot read: two versions ran against the same project, such as
a global csszyx beside the one in node_modules. The shard is left on disk
for the session that wrote it, and its classes get no CSS from this one until
the prebuild of the installed version writes the shard again. A field a newer
version adds to a shard does not change its format, so this only follows a
change an older reader cannot read past.
The sz build diagnostics under Turbopack
Section titled “The sz build diagnostics under Turbopack”The Turbopack loader prints the same sz build diagnostics as the Vite and
webpack lanes, at the same levels: warn and error in
every mode, info only under next dev. It reads the levels from
.csszyx/diagnostic-policy.json, which csszyx next prebuild and
csszyx next watch write from csszyx.config. They arrive as Turbopack warnings on the
module, so Next prints them under the file’s path in the terminal, shows them in
the dev overlay, and drops them once the module compiles clean. A production
build lists none of the advisories and, unlike the other lanes, prints no count
of them, because a loader never sees the build end; run
csszyx check for the full list.
The loader takes no quiet option. CSSZYX_QUIET_SZ_WARNINGS=1 in the
environment of next dev or next build mutes what quiet: true mutes on the
other lanes.
The Tailwind prefix on the Next.js lanes
Section titled “The Tailwind prefix on the Next.js lanes”[csszyx] next-prefix-unread: The Turbopack loader has no Tailwind prefix for `{root}` ({reason}), so it stops. help: run `csszyx next prebuild` before `next build`, and `csszyx next watch` beside `next dev`; both read the stylesheets first. note: this module was not transformed. docs: https://csszyx.com/e/next-prefix-unreadA build error from the Turbopack loader, and from runNextPrebuild called
from a script, which reads “The Next prebuild has no Tailwind prefix” and asks
for prepareNextStylesheetFacts first. Those
lanes read the prefix from .csszyx/cache/stylesheet-facts.json, and it was
missing, or a stylesheet changed after it was written. Run the command the help
line names. A next dev loader with no watcher reads the stylesheets itself
instead of stopping.
[csszyx] internal-error: An internal check failed in `transformNextSource`, so csszyx stops. help: report this csszyx bug at https://github.com/nguyennhutien/csszyx/issues with this message, the csszyx version and the bundler or command you ran. note: the check that failed: a Next build asked the engine to transform a module before it read the project's Tailwind prefix. docs: https://csszyx.com/e/internal-errorShould never reach you. Report it with the Next.js version.
An sz class produces no CSS
Section titled “An sz class produces no CSS”[csszyx] dead-class: {file}:{line}:{column} `{className}` produces no CSS: this project's Tailwind serves no such utility. help: fix the sz key or value, or define the class with Tailwind's `@utility`; `csszyx check --rule dead-class` lists every one. note: the class is still emitted, and it styles nothing.The build-time dead-class finding, the same id and the same answer as
csszyx check. { break: 'bogus' } lowers to break-bogus and
{ justify: 'safe-center' } to justify-safe-center: both keys are real, so
no key warning fires, and the class sits in the DOM styling nothing. The build
already compiles the project’s Tailwind to settle the merge table, and asks it
about every class an sz prop emitted; {file} is the first file, in path
order, that emits the class and whose level lists it — an overrides entry
that turns dead-class off for one file, or holds it at info in a production
build, moves the line to the next file that emits the class. A class no file
lists is counted once in the production closing line when any file holds it. The line and column are where the sz key that emits it is written, on every lane —
Vite, Rollup, webpack, and under Turbopack through csszyx next prebuild and
csszyx next watch — for a class that is the only one of its object as for one
of several. A class with no recorded site (a value only known at runtime) names
the file alone.
Printed by Vite and Rollup builds (renderStart), webpack (finishModules),
and under Turbopack by csszyx next prebuild and csszyx next watch, which
settle the table there (CSSZYX_QUIET_SZ_WARNINGS=1 mutes them there, as it
mutes the loader). It is error under both presets: printed in every mode,
never failing the build — gate on csszyx check, which exits 1 on it. Only
classes an sz prop emitted are asked about; names written in className are
the app’s own and are never reported here. A class the project defines on
purpose goes in diagnostics.allow.classes; diagnostics.rules['dead-class']
set to off hides the line.
No CSS at all
Section titled “No CSS at all”[csszyx] tailwind-stylesheet-absent: This build emitted {count} class(es) for `sz` props and processed no Tailwind stylesheet, so none of those classes produces CSS. help: add `@import "tailwindcss";` to a stylesheet the app loads; csszyx adds the `@source` for its safelist there itself. docs: https://csszyx.com/e/tailwind-stylesheet-absentPrinted once per build, in production too, when csszyx emitted classes and CSS
passed through the plugin but none of it imported tailwindcss.
Some files skipped
Section titled “Some files skipped”| Message | Notes |
|---|---|
[csszyx] compile-sources-skipped:, then The build skipped {count} file(s) under `packages/` that use csszyx, so it compiles none of their `sz` props and none of their classes reach the safelist. — help: add the package directory to `compileSources`, such as `compileSources: ['packages/ui']`, or move the files out of `packages/`. — note: the skipped files: and one project path per line | Normally a nudge, in development only. When any skipped file may export szv factories it fires in production too, and the note opens: of these, {szvCount} may export `szv` factories, so another module that calls one runs it at runtime instead of getting its classes compiled in; the skipped files: |
[csszyx] ast-budget: {file}, then The prescan skipped this file because it exceeds the AST node budget, so none of its classes reached the safelist. — help: split the file, or raise `build.astBudgetLimit` in the plugin options; the Turbopack loader takes no budget option, so split the file there. — note: its classes produce no CSS unless another source names them. | At the level diagnostics.rules gives ast-budget; listed in every mode at the default |
[csszyx] engine-call-failed:, then The prescan could not transform `{file}`, so none of its classes reached the safelist. — help: fix the error the transform reported; under Tailwind `source(none)`, no class of this file gets CSS until then. — note: the transform reported: {detail}. | Ungated |
[csszyx] parse-error: {file}, then The prescan could not parse this file, so none of its classes reached the safelist. — help: fix the syntax error, or check that the file extension matches its contents. — note: its classes produce no CSS unless another source names them. | At the level diagnostics.rules gives parse-error |
[csszyx] safelist-capped:, then The safelist reached its cap of {max} classes, so the build left out every class it found after that. — help: look for `sz` values that write a different arbitrary value per element, such as `w-[137px]`, and replace them with theme values. — note: the classes left out produce no CSS unless another source names them; the cap is fixed, and no option raises it. | Warning, once per build |
.gitignore cannot be read
Section titled “.gitignore cannot be read”[csszyx] ignore-package-incompatible: The installed `ignore` package lacks the internal matcher csszyx calls, so csszyx cannot read `.gitignore` and stops. help: remove any override or resolution that forces another version of `ignore`, so csszyx gets `[email protected]`, the version it pins. docs: https://csszyx.com/e/ignore-package-incompatibleThe build stops, and the help: line says what to do. csszyx reads .gitignore the way Tailwind does, folder by folder, through a part of the ignore package that is not public, and a version of ignore other than the one csszyx pins has removed it. A lockfile override (pnpm.overrides, npm overrides, Yarn resolutions) that forces another version is the usual cause.
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:, then This build parses with the native engine. · This build parses with the wasm engine. · This build parses with the wasm engine in place of the default native engine. |
[csszyx] native-engine-unavailable:, then The native engine is not available here, so the default `rust` parser runs on the wasm engine. — help: reinstall without skipping optional dependencies to get the native engine, or set `build.parser: 'wasm'` to hide this line. — note: the wasm engine produces the same output; only parsing is slower. |
[csszyx] parse-error: Syntax error: {message}. — help: fix the syntax at this position. — note: the file is left unchanged. One per syntax error, carrying the parser’s message at the place it points to. A file whose sz values are all flat static objects, such as sz={{ p: 4 }}, takes a fast path that lowers them without parsing the file, so csszyx reports no syntax error in it: the bundler or tsc does. |
Budgets and limits
Section titled “Budgets and limits”| Message | Kind |
|---|---|
Thrown. [csszyx] sz-object-depth: This `sz` value nests deeper than {maxDepth} levels, so csszyx rejects it. — help: check what reaches the `sz` prop; nesting this deep usually means untrusted or looping data. | Error: an SzDepthError, with code sz-object-depth |
[csszyx] nesting-depth: Source nesting exceeds {maxNestingDepth} levels (found {depth}). — help: flatten the structure; nesting this deep usually comes from `sz` or JSX over-nested by hand or by generated code. — note: the file is left unchanged and contributes no classes. | Native engine |
[csszyx] ast-budget: This file exceeds the AST node budget. — help: split the file, or raise `build.astBudgetLimit` in the plugin options; the Turbopack loader takes no budget option, so split the file there. — note: the file is left unchanged, and none of its classes are collected. | 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”One record per kind of hazard, each with its own fix. Printed when mangled tokens collide with class names in CSS csszyx does not own:
[csszyx] mangle-hybrid-hazard: Mangling renamed classes to {count} token(s) that CSS outside csszyx also uses as class names, such as `{names}`, so the renamed elements pick up that CSS and the elements it was written for pick up the csszyx rules. help: rename those classes in your own CSS to something specific, such as `.x` to `.resize-handle-x`, and list the ones in a stylesheet you cannot edit in `production.mangleExclude`; until then, pass `production: { mangle: false }` to the csszyx plugin. note: `csszyx scan-collisions` lists every class name in your stylesheets that a token could take, as a `mangleExclude` list you can paste. docs: https://csszyx.com/e/mangle-hybrid-hazardWhen a mangled class has no rule in any stylesheet of the build, the record
reads Mangling renamed {count} class(es) that no stylesheet in this build has a rule for, such as `{names}`, so a stylesheet outside this build that styles them by name, such as one a separate Tailwind run writes, no longer matches them. — help: build that stylesheet through this bundler, so csszyx renames its rules too; until then, pass `production: { mangle: false }` to the csszyx plugin.. Each record names up to eight classes and counts all of them.
A stylesheet that matches classes by their text is the third hazard. The record
names the selector and the classes it was matching, and the remedy is a
paste-ready production.manglePreserve:
[csszyx] mangle-hybrid-hazard: Mangling broke {count} attribute selector(s) that match class names by text, such as `{selectors}`, so those rules stop matching and the elements lose those styles. help: keep those classes readable with `production.manglePreserve: [{entries}]`, or select on a data attribute, which mangling never changes. note: `production.mangleExclude` does not help here: it reserves token names and still lets the class be renamed. docs: https://csszyx.com/e/mangle-hybrid-hazardEach of up to three selectors is 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
record reads Mangling made {count} attribute selector(s) match the tokens it renamed classes to, such as `{selectors}`, so their rules style elements they were not written for. — help: reserve those tokens with `production.mangleExclude: [{entries}]`, or select on a data attribute, which mangling never changes.
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] mangle-preserve-invalid: `production.manglePreserve[{index}]` is not a class name or a prefix (got {got}), so the build stops. help: write an exact class name, such as `bg-tag-red`, or a prefix ending in `*`, such as `bg-tag-*`.[csszyx] mangle-preserve-invalid: `production.manglePreserve` holds a lone `*`, which would keep every class unmangled, so the build stops. help: name a prefix, such as `bg-tag-*`, or set `production.mangle: false` to turn mangling off.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] internal-error: An internal check failed in `assertGlobalVarPlanMatchesEarlyAliases`, so csszyx stops. help: report this csszyx bug at https://github.com/nguyennhutien/csszyx/issues with this message, the csszyx version and the bundler or command you ran. note: the check that failed: the CSS alias plan {actual} does not match the source alias table {expected}. docs: https://csszyx.com/e/internal-errorThe names the stylesheet was rewritten with and the names the source transform
recorded have to be the same set. Both read the same tokens, so no setting
reaches this; when they differ, 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”Every row prints in production too, except uncompiled-sz; there each prints
its code, its args as JSON and the code’s page instead of the sentence.
| Message | Notes |
|---|---|
[csszyx] mangle-map-unreadable: The class census on this page (`__CSSZYX_MANGLE_MAP__`) is not a map of class names to class names, so it is ignored. — help: serve the HTML as the build wrote it; a census that cannot be read means the page was changed or cut short after the build. | Printed through console.error, in production too. |
[csszyx] mangle-map-unreadable: The class census on this page (`__CSSZYX_MANGLE_MAP__`) is not valid JSON, so it is ignored. — help as above — note: the browser reported: {error}. | The census is not JSON; it is ignored. |
[csszyx] recovery-manifest-unreadable: The recovery manifest on this page is not valid JSON, so it is ignored. — help: serve the HTML as the build wrote it; the manifest is the `__SZ_RECOVERY_MANIFEST__` script the build adds to the page. — note: the browser reported: {error}. | From loadManifestFromDOM, through console.error, in production too. |
[csszyx] mangle-checksum-mismatch: The mangle map on this page hashes to `{computed}`, and the checksum it was built with is `{expected}`, so the check fails. — help: deploy the HTML and the bundle from one build. — note: this detects a damaged or stale page, not an attacker; the checksum is not signed. | From verifyMangleChecksumAsync, which answers false. The census or the bundle’s map was altered, or the HTML and the bundle come from different builds. |
[csszyx] mangle-map-unavailable: This page carries the checksum `{checksum}` but neither a class census nor a bundled mangle map to recompute it from, so nothing was verified. — help: keep `production.hydrationCensus` on, its default, when the build sets `production.mangleVars` without `production.mangle`, since such a build bundles no map; otherwise, load the bundle that registers the map on this page. | From verifyMangleChecksumAsync, which answers false. Turning mangling off verifies quietly, and so does turning the census off while classes are mangled: the bundle then installs the map. Only mangleVars without class mangling and without the census leaves nothing to check. |
[csszyx] mangle-census-unreadable: The class census on this page could not be read, so the mangle checksum was not verified. — help: serve the HTML as the build wrote it; the line before this one says what is wrong with the census. | From verifyMangleChecksumAsync, which answers false without reading past the census to the bundle’s map: something between the build and the browser changed or cut short the __CSSZYX_MANGLE_MAP__ script, which is the damage the check exists to catch. |
[csszyx] web-crypto-unavailable: Web Crypto is unavailable, so csszyx cannot recompute the mangle checksum. — help: serve the page over HTTPS, or from `localhost`. — note: `crypto.subtle` exists only in a secure context; `verifyMangleChecksumAsync` answers `false` without checking. | verifyMangleChecksumAsync answers false, in production too; computeMangleChecksumAsync throws. |
[csszyx] hydration-aborted: The HTML, the bundle and the recovery manifest do not all come from one build, so csszyx aborted hydration at `<{tagName}>`. — help: deploy the HTML, the bundle and the manifest from one build. | Through console.error, with the element after the message. A call to abortHydration with map_missing or invalid_token names that type and adds note: the caller reported: {message}. |
[csszyx] csr-recovery-unmarked: This element has a hydration mismatch but no `szRecover` directive, so it is not recovered on the client. — help: add `szRecover="csr"` to recover it, or `szRecover="dev-only"` to recover it in development only. | With the element after the message. |
[csszyx] csr-recovery-dev-only: This element has `szRecover="dev-only"`, which is off in production, so its hydration mismatch is not recovered. | By design. Only a production build prints it, so it prints as [csszyx] csr-recovery-dev-only: {"form":"runtime"} https://csszyx.com/e/csr-recovery-dev-only, with the element after it. |
[csszyx] csr-recovery-used: The hydration-abort marks on this element were cleared, so your code can render it on the client. — help: fix the cause of the mismatch; with `szRecover="csr"`, this recovery also runs in production. | The page renders, but you have a real mismatch. |
[csszyx] csr-recovery-dev-only:, then The production manifest leaves out {count} `szRecover="dev-only"` token(s), so recovery for those elements is off in production. — help: use `szRecover="csr"` on any of them whose recovery must run in production. — note: this is expected in a production build; the tokens are at {sites}. | Build-time, informational, once per build however many HTML entries it has. |
[csszyx] uncompiled-sz: A raw `sz` object reached the runtime because its file was not compiled, so it produces no CSS. — help: add the package directory to `compileSources` when the file lives in a workspace package; otherwise, check that the bundler does not skip the file. — note: csszyx dropped the object, so the DOM does not get `sz="[object Object]"`. | 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. |
✖ {n} sz issue(s) in {m} file(s); {k} below --fail-on {level}, which do not fail the run. | Some issues are at a level quieter than --fail-on; they are marked (info) or (warn) in the report. The line starts with ! instead of ✖ when none of them fails the run. |
✖ [csszyx] cli-option-invalid: `--fail-on {value}` is not a level this command takes, so the command did not run. — help: pass `--fail-on info`, `--fail-on warn` or `--fail-on error`, or leave it out for `error`. | The run fails without scanning. off is refused too: an off finding is never reported, so it cannot be a threshold. |
✖ [csszyx] unknown-rule-id: No rule or diagnostic is named `{ids}`, so the selection would match nothing and `csszyx check` stops. — help: write `{suggestion}` if you meant it; https://csszyx.com/docs/reference/warnings/ lists every id. | A --rule or --ignore-rule id that is no pass and no diagnostic. The run fails without scanning. With one such id the help names the nearest known one; with several it says check the spelling instead. |
Scope: static `sz` props and `szv()`/`szr()` catalogs; keys built from runtime data (an array, a spread, a `dynamic()` value) cannot be checked. | 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) produce no CSS: fix the sz key or value, or define the class with Tailwind's `@utility`. | Each offender is listed above it, with the file that first emitted it. In --json each is a dead-class finding with the registry’s message (\`{className}\` produces no CSS: this project's Tailwind serves no such utility.), help, note, args and url. |
Negative values this project's Tailwind does not serve: + one line per class | The dead classes whose utility carries a sign sz hoisted (-p-2) and whose unsigned form is served, with the file that first emitted each; rule dead-class, kind unsigned-value in --json. Not listed again under the dead classes. |
✖ {n} emitted class(es) produce no CSS: this project's Tailwind serves no negative form of their utility; drop the sign, or define the negative with Tailwind's `@utility`. | Printed under that list. ! instead of ✖ when none of them fails the run. |
Variants this project's Tailwind does not serve: + two lines per class | The dead classes served with one variant dropped (hidden:p-4), with the file that first emitted each and a help: line for the name; rule dead-class, kind unserved-variant in --json, with help, note and args (form, variant, className). Not listed again under the dead classes. |
✖ {n} emitted class(es) produce no CSS: each uses a variant this project's Tailwind does not serve. | Printed under that list. ! instead of ✖ when none of them fails the run. |
Dead-class check skipped: this project has no Tailwind stylesheet. | The check needs a Tailwind entry to ask. |
Dead-class check skipped: {reason}. | |
✖ A Tailwind stylesheet of this project did not compile (named above), so the dead-class check did not run and `csszyx check` fails until it compiles. | Exits non-zero. See below. |
Files given to `--files` that do not exist: + 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: 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: pass a directory under it, or leave `[dir]` out to check the whole project. | Exits non-zero rather than passing on the zero files a missing directory would match. |
✖ Could not list the files to check: {reason} | The glob itself failed; exits non-zero. |
✖ {n} file(s) given to `--files` do not exist, so no file was checked: fix each path above; a relative path is read from `--cwd`, or the current directory. | Exits non-zero, before any file is scanned. |
Values that belong to a different sz key: + two lines per offender, the second reading \`{key}: '{value}'\` emits \`{className}\`, which sets {sets}, not the property \`{key}\` sets. | A value written on a key that owns neither it nor its CSS property: the registry’s sibling-keyword sentence; --json adds its help, note, args and url. |
✖ {n} value(s) emit a class that sets another property than their key: move each to the key that sets that property, or declare a theme token named after it if the spelling was deliberate. | Exits non-zero at error. |
Theme tokens a built-in utility already claims: + two lines per token, the second reading Theme token \`{name}\` also names \`{classes}\`, so Tailwind compiles both meanings into one rule, which sets the token's property and the built-in's. | Reported at the declaration, with its line: the registry’s theme-collision sentence; --json adds its help, note, args and url. |
✖ {n} theme token(s) share a name with a built-in utility: rename them, or keep one with `--allow-token <name>` or `diagnostics.allow.tokens` in `csszyx.config`. | Exits non-zero at error. |
Opacity modifiers that do not apply: + three lines per class: the class and its origin, the registry’s broken-opacity sentence, and its help: line | A theme token that resolves to a bare comma triplet; see below. |
✖ {n} emitted class(es) carry an opacity modifier that does not apply. | Printed under that list. ! instead of ✖ when none of them fails the run. |
Classes the build removes because an `sz` class on the same element covers them: + one line per class reading {file}:{line}: {sentence}, then note: a build with \build.mergeCoveredClasses: false` keeps these.` | The merge audit (--rule merge-covered-key, --rule merge-covered-class, or a config that reports them at warn): the registry’s merge-covered-key / merge-covered-class sentence for each class. |
! {n} class(es) are removed because an `sz` class on the same element covers them. | Printed under that list; ✖ when a config raises one of them to --fail-on or louder. |
✖ Merge audit did not run: {reason} | The audit was selected and the project’s stylesheets would not open — one did not compile, or they set different prefixes; for different prefixes a help: line follows, give every Tailwind stylesheet the same `@import "tailwindcss"` line. Exits non-zero, as the dead-class pass does: an audit that could not run must not pass. |
Every pass marks its findings the way the sz-issue report does. A finding
quieter than error is listed with (info) or (warn) in front of it. While
any finding of the pass is at --fail-on or louder its summary starts with
✖; when none is, it starts with ! instead, and every summary with such
findings ends {k} below --fail-on {level}, which do not fail the run.
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. A colour class written after it does not
cover text-wrap, so szcn keeps both, 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:
[csszyx] tailwind-v3-unavailable: `csszyx generate-types` reads a Tailwind v3 config, and this project has no Tailwind installed. help: install `tailwindcss@3` as a dev dependency, such as `npm install -D tailwindcss@3`; on Tailwind v4 the theme lives in CSS under `@theme`, and this command is not needed. note: `@csszyx/cli` lists Tailwind v3 as an optional peer, so installing the CLI does not install it.Tailwind is installed, but it is v4 (or any major other than 3):
[csszyx] tailwind-v3-unavailable: `csszyx generate-types` reads a Tailwind v3 config, and this project has Tailwind {version}, which has no JavaScript config to read. help: declare the theme in CSS under `@theme`, which needs no generated types; to type a v3 config left over from a migration, run `npx -p tailwindcss@3 -p @csszyx/cli csszyx generate-types --config ./tailwind.config.js`. note: `tailwind.config.js` and the `resolveConfig` helper this command calls were removed in Tailwind v4.v3 is installed and its entry did not load — a patched copy, a blocked exports map, a damaged file:
[csszyx] tailwind-v3-unavailable: `csszyx generate-types` found Tailwind {version} but could not load its `resolveConfig` entry, so it cannot read the config. help: reinstall it, such as `npm install --force tailwindcss@3`; if the error stays, report this csszyx bug at https://github.com/nguyennhutien/csszyx/issues with this message. note: loading the entry reported: {reason}.A config path the command cannot read stops it the same way, headed by the path: tailwind-config-missing (This Tailwind config does not exist, so `csszyx generate-types` cannot read the theme.) for a --config that names no file, the same code headed by no path (No Tailwind config is in `{root}`, so `csszyx generate-types` cannot read the theme.) when no --config was passed and the working directory has none, and tailwind-config-unloadable (This Tailwind config threw while loading, so `csszyx generate-types` cannot read the theme., with what the import threw) for one that throws.
The same messages are thrown as csszyx errors 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”Opacity modifiers that do not apply: {className} {file} `{className}` carries an opacity modifier that does not apply: its theme token resolves to the bare comma triplet `{value}`, which `color-mix()` cannot dim. help: wrap the token's variable in a colour function, such as `rgb(var(--your-triplet))`.
✖ {n} emitted class(es) carry an opacity modifier that does not apply.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. Each class is listed with its origin and the registry’s
broken-opacity words.
✖ {n} file(s) given to `--files` do not exist, so no file was checked: fix each path above; a relative path is read from `--cwd`, or the current directory.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:
[csszyx] cli-option-invalid: `--debounce-ms {value}` is not a whole number of milliseconds from 0 to 60000, so the command did not run.
--mode on next-prebuild and --parser-mode on both are refused the same
way, each under its own form of cli-option-invalid; csszyx init --framework
with a value no framework has is too
([csszyx] cli-option-invalid: `--framework {value}` is not a framework `csszyx init` knows, so the command did not run.),
before it asks or installs anything. A pattern that matches no source file
stops both Next commands with
[csszyx] no-source-files: No source file under `{root}` matches `{pattern}`, so there is nothing to scan for classes.,
and a watcher that throws while closing ends next watch with exit code 1 and
[csszyx] next-watcher-unclosed: The Next safelist watcher failed while closing, so `csszyx next watch` exits with code 1.
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 info findings only as a count; check lowers every
source file the same way the build does and lists each finding with its file and
line, whatever NODE_ENV says.
Every finding has a level — info, warn or error — from
csszyx.config. The run exits 1 when
at least one finding is error, or when the run itself failed: files that could
not be read, a stylesheet that would not compile, a config with an id it does
not know. info and warn findings are reported and the run still passes.
- name: csszyx run: npx csszyx checkUnder the default recommended preset every finding fails the run except the
ones that report a choice or an audit rather than a mistake. info:
class-precedence says sz wins over a runtime className, duplicate-sz
says which order two sz attributes merged in, and merge-covered-key and
merge-covered-class say what a merge removed. warn:
merge-classifier-unavailable, where every class is kept and the output is only
less merged. The preset also makes fallback-nudge and
mangle-vars-hoist-skipped info, but neither is a check finding. To fail on
any of them, set it to error in csszyx.config, or pass --fail-on info;
--fail-on warn fails on warn and error findings. 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
{ "$schema": "https://csszyx.com/schemas/check-v2.json", "version": 2, "findings": [...] },
one entry per finding. The schema
describes every field; validate against it when a later step depends on the
shape.
{ "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", "level": "error"}An sz-diagnostic finding carries the main sentence as message, and the
clauses the terminal prints after help: and note: as help and note,
each left out when the diagnostic has none. None of the three holds a line
break or a label. column is 1-based, in UTF-16 code units; args holds the
values the sentence names; url is the kind’s page.
{ "rule": "sz-diagnostic", "kind": "unknown-key", "file": "src/App.tsx", "line": 1, "column": 35, "message": "`workBreak` is not an sz property.", "help": "write `break` if you meant it; for a custom utility, define it with Tailwind's `@utility`.", "note": "the class is still emitted, and it styles nothing unless Tailwind serves that utility.", "suggestion": "break", "args": { "key": "workBreak", "suggestion": "break" }, "url": "https://csszyx.com/e/unknown-key", "level": "error"}level is the level the project’s config gives the finding. A finding set to
off is not in the document.
rule is one of sz-diagnostic, dead-class, broken-opacity,
sibling-keyword, theme-collision, merge-covered-key,
merge-covered-class. 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.
merge-covered-key and merge-covered-class are an audit, not a problem: they
list each class the build drops because a later one on the same element covers
it — a later key in one static sz object, or an sz class over a static
className — one finding per removed class, with the line and the key it was
written as. They run when named with --rule, or when csszyx.config sets
them to warn or louder (the atomic preset does). Their findings are info
under recommended, so they fail the run only where the config sets them to
error:
csszyx check --rule merge-covered-key --rule merge-covered-classkind says what an sz-diagnostic found: unknown-key, canonical-key,
removed-key, numeric-key, closed-enum-value, slash-opacity-string, color-slash-opacity, 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, spread-split-class, moved-value, global-before-group,
runtime-family-conflict, merge-classifier-unavailable, parse-error,
nesting-depth, unsupported-dynamic-sz or ast-budget. The engine gives
every diagnostic its kind, so other never comes back for one; it is kept
only as an id --rule, --ignore-rule and rules still accept, with a
warning. Every other pass repeats its rule,
but for three kinds of dead-class: unsigned-value, a dead class whose sign
the project’s Tailwind serves no negative for, unserved-variant, a dead
class whose variant the project’s Tailwind does not serve, and
prefix-disagreement.
fallback-missing-css is listed too: its classes were never collected, so
they have no CSS. The fallback that still collected its classes
(fallback-nudge) and the mangleVars hoist note
(mangle-vars-hoist-skipped) are not check findings, so they never appear
here.
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 help: write `{key}` if you meant it
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. They select findings and never set a level: a selected
info finding still passes. An id that is neither a
rule nor a kind fails the run rather than selecting nothing:
✖ [csszyx] unknown-rule-id: No rule or diagnostic is named `{ids}`, so the selection would match nothing and `csszyx check` stops. help: check the spelling; https://csszyx.com/docs/reference/warnings/ lists every id.✖ The Tailwind stylesheets of this project set different prefixes (`{file}` with `prefix({name})`, `{file}` with none, …), so no class csszyx emits is served by all of them. help: give every Tailwind stylesheet the same `@import "tailwindcss"` line. note: the dead-class check did not run.A dead-class finding of kind prefix-disagreement, and exit code 1: every
class is dead under one of the prefixes, so there is no answer to report. The
check skips the same build output the build walk does (dist, build,
target, coverage, storybook-static and the framework caches), so a stale
copy there does not cause it. --ignore-rule prefix-disagreement, or a
--rule list without it, leaves the finding out, and the dead-class pass is
skipped instead of run under a guess:
Dead-class check skipped: the Tailwind stylesheets set different prefixes, and `--rule` or `--ignore-rule` leaves `prefix-disagreement` out of this run.A csszyx.config that sets prefix-disagreement to 'off' silences the
finding, not the pass: the dead-class check runs with the prefix of the first
stylesheet, and reports a class only when no stylesheet serves it.
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] lowering-not-loaded: A string helper received an sz object, but the module that turns sz objects into class names is not loaded, so it cannot produce class names for it. help: add `import '@csszyx/runtime/lowering';` once at startup when the code runs outside the csszyx bundler plugin, such as in unit tests or scripts; the plugin loads it for files that can pass objects at runtime.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] lite-sz-object: `{fnName}()` takes class strings and received the plain object `{received}`, because the compiler could not resolve this `sz` prop at build time. help: write `sz={[cond ? varA : varB, { key: 'val' }]}`; a spread of a conditional, such as `sz={{ ...(cond ? varA : varB), key: 'val' }}`, is the usual cause. note: a production build skips this check: `_sz` returns the object, or `[object Object]` beside other classes, and `_szMerge` throws a `TypeError`.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] split-box-selector-invalid: `has`, `pick` and `omit` take one selector, so an array matches nothing. help: pass the selectors one at a time; `splitBox` takes a list in `inner` and `outer`.[csszyx] split-box-selector-invalid: The empty selector `{}` names no category, so it matches nothing. help: name a category and a value, such as `{ overflow: "hidden" }`.[csszyx] split-box-unknown-category: `{name}` is not a category or class prefix csszyx knows, so it matches nothing. help: use the category `{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] split-box-unknown-category: `{name}` is not a category or class prefix csszyx knows, so it matches nothing. help: use the category `{hint}`, or `{hint}:{name}` to match that property only.[csszyx] split-box-unknown-category: `{name}` is not a category or class prefix csszyx knows, so it matches nothing. help: call `classify('<a class>')` to see the category a class belongs to.[csszyx] split-box-selector-invalid: An object selector names one category and value, so `{ {categories} }` can never match a single token. help: pass one selector per category.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, with a
TypeError in development:
[csszyx] split-box-sz-string: `splitBoxSz` partitions sz objects, not class strings, so it cannot split `{value}`. help: use `splitBox()` for a class string. note: a production build drops the string instead of throwing.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] split-box-selector-invalid: `{selector}` names a property, and `hasSz`, `pickSz` and `omitSz` match an sz key by category only, so it matches nothing. 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] split-box-peer-on-frame: `splitBox` keeps `{token}` 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: target the content from the frame instead: `{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.
css went to the fallback node
Section titled “css went to the fallback node”[csszyx] split-box-unclassified: The `css` key holds raw CSS `splitBoxSz` does not classify, so it went to the {node} node with everything else it could not classify. help: if its declarations belong on the other node, place it yourself with `{ {side}: ['css'] }`.css carries raw declarations, not sz keys, so splitBoxSz cannot tell which
node each one belongs to. It keeps the whole object together on the fallback
node — the frame by default, the content node with fallback: 'inner' — as
splitBox does with the [property:value] classes it emits, rather than split
it by declaration name. Naming css in inner or outer places it and
silences the warning.
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] split-box-selector-invalid: `{property}` is not a property csszyx tells apart, so `{selector}` matches nothing. help: use one of `{properties}`; `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] split-box-unbounded-scroll: `splitBox` put `{scroller}` on the content node, but nothing bounds the height of either node, so the content grows 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] split-box-unclipped-corners: The `splitBox` 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] split-box-hidden-content: `splitBox` put `{hidden}` on 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] split-box-unclassified: `{token}` is not a utility csszyx knows, so it went to the {node} node with everything else it could not classify. help: if it belongs on the other node, place it there with `{ {side}: ['{token}'] }`; a custom `@utility` that sets properties of both nodes has no correct side, so split it into two utilities.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 {side} 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] split-box-placement-variant: A `splitBox` placement list names a class by its base, so `{selector}` 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] diagnostics-capped: This session has printed {count} distinct development warnings, so further ones are suppressed. help: look above for one warning repeating with different values; a `className` built from data is the usual cause.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 a key no table knows as a squiggle, with the
code the build gives it — removed-key, canonical-key,
numeric-key or unknown-key — and the same sentence, help: and note: the
build prints, filled from the same templates. The code in the Problems panel
links to its https://csszyx.com/e/<code> page. Turn them off with
csszyx.enableDiagnostics: false.
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”A Vue or Svelte template has no csszyx runtime, so the adapters write a class
only for an sz value the engine resolves completely at build time. Any other
value is an error placed at its position in the file, its attribute is left as
written, and the build stops:
[csszyx] template-runtime-value: {file}:{line}:{column} This `sz` value is only known at runtime, and a template has no csszyx runtime to resolve it, so it gets no class. help: write the value as a literal, such as `{ p: 4 }`, or bind `class` or `style` yourself for a value that changes at runtime. note: the attribute is left as written. docs: https://csszyx.com/e/template-runtime-valueA name, a prop, a conditional, a call, an array value, or a template string
with ${} is only known at runtime. A value with a syntax error is
parse-error in its expression form: “This sz value
could not be parsed as a JavaScript expression.” The errors of one file fail
the build at once, headed by their count:
[csszyx] template-sz-invalid: {file} This file has {count} `sz` error(s), so the build stops. help: fix each error listed below. docs: https://csszyx.com/e/template-sz-invalidA file the framework cannot read is one error, and no sz in it is lowered:
| Message | When |
|---|---|
[csszyx] template-unparsable: Vue cannot parse this file, so no `sz` attribute in it gets a class. — help: fix the error the Vue compiler reports; the file does not compile without csszyx either. — note: the Vue compiler reported: {message}. | vue/compiler-sfc rejects the file, an element left open say |
[csszyx] template-unparsable: Svelte cannot parse this file, so no `sz` attribute in it gets a class. — help: write a quoted sz object as an expression, `sz={{ p: 4 }}`; Svelte does not parse `sz="{ p: 4 }"`. | svelte/compiler rejects a component that quotes an sz object |
[csszyx] template-unparsable: Svelte cannot parse this file, so no `sz` attribute in it gets a class. — help: run csszyx after the preprocessors that turn this file into Svelte, such as TypeScript without `lang="ts"`, or pug. | svelte/compiler rejects any other component: TypeScript without lang="ts", or markup another preprocessor has not turned into Svelte yet |
[csszyx] template-lang-unsupported: This template is `<template lang="{lang}">`, and csszyx reads `sz` in HTML templates only, so no `sz` attribute in it gets a class. — help: write the template in HTML, or bind `class` yourself in it. | a Vue template in another language, such as pug |
[csszyx] svelte-version-unsupported: Svelte {version} resolves here, and csszyx reads Svelte 5 markup only, so no `sz` attribute in this file gets a class. — help: install `svelte@5`. | the project installs Svelte 4 |
An adapter loads its framework’s parser from the project the first time a file needs it. When none resolves, the build stops on:
[csszyx] peer-module-unresolved: `@csszyx/vue-adapter` reads templates with `vue/compiler-sfc`, which does not resolve from it, so no template is read. help: install `[email protected]` in the project. docs: https://csszyx.com/e/peer-module-unresolvedThe Svelte adapter names svelte/compiler and svelte@5 the same way. The
engine’s warnings for a value that did lower, such as an unknown key, are
reported at the same kind of position as warnings.
The Vite plugins and @csszyx/unplugin fail the build on an error and pass
warnings to the bundler. The Svelte preprocessor throws with every error of the
file and prints warnings. preprocess() from either adapter reports nothing
itself: it returns them in result.diagnostics.
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.
The compiler and the engine come from different releases
Section titled “The compiler and the engine come from different releases”[csszyx] engine-version-skew: `@csszyx/compiler` {expectedVersion} loaded the {artifact} engine {foundVersion}, and the two must come from the same release, so csszyx stops. help: install `@csszyx/core@{expectedVersion}`; it installs its platform package at the same version. docs: https://csszyx.com/e/engine-version-skew{artifact} is native or wasm. The sentence and its help: name the fix
for what answered:
| Message | When |
|---|---|
[csszyx] engine-version-skew: `@csszyx/compiler` {expectedVersion} loaded the native engine {foundVersion} from `{platformPackage}`, and the two must come from the same release, so csszyx stops. — help: install `@csszyx/core@{expectedVersion}`, and reinstall `{platformPackage}` if your lockfile holds it at another version. | the native engine of another release: a lockfile can keep an older platform package beside a current @csszyx/core |
[csszyx] engine-version-skew: `@csszyx/compiler` {expectedVersion} loaded a {artifact} engine from a release before 0.18, and the two must come from the same release, so csszyx stops. | an engine too old to report its version |
[csszyx] engine-version-skew: The {artifact} engine was built from source `{foundDigest}`, and `@csszyx/compiler` {expectedVersion} expects `{expectedDigest}`, so csszyx stops. — help: rebuild the engine from this checkout: `pnpm --filter @csszyx/core native:build` for native, `pnpm --filter @csszyx/core build` for wasm. | an engine at the compiler’s version built from other source, which happens only in a checkout of csszyx itself |
@csszyx/compiler and the engine in @csszyx/core are released together but
installed apart, so a lockfile or a cache can pair them across releases. An
engine from another release ignores options it does not know and lowers with
other tables, so the first call into either artifact checks the version and the
digest of the engine’s source, and the build stops on a mismatch. There is no
setting that skips the check. Install @csszyx/core at the compiler’s version;
@csszyx/unplugin, @csszyx/cli and csszyx pin it for you. When both sides
report the same version, the engine and the compiler were built from different
checkouts of csszyx itself: rebuild the engine.
The plugin and the runtime come from different releases
Section titled “The plugin and the runtime come from different releases”[csszyx] runtime-version-skew: `@csszyx/unplugin` {pluginVersion} found `@csszyx/runtime` {runtimeVersion} at `{runtimeDir}`, and the two must come from the same release, so the build stops. help: install `@csszyx/runtime@{pluginVersion}`.The build lowers every static sz object, and @csszyx/runtime lowers the rest
in the browser — szr, szv, objects built at run time — and merges classes
with the table the build wrote. Each side carries its own copy of the rules, so a
runtime from another release writes other classes for the same object, and
neither output says so. The plugin reads the @csszyx/runtime your project
root resolves when the build starts (on Turbopack, in csszyx next prebuild and
in the loader) and stops when its version differs. A package that pins its own
copy of the runtime is not compared. Install the runtime at the plugin’s
version; with pnpm, pnpm why @csszyx/runtime shows which package pulled in the
other copy.