ReviewOS

stacks/stx

preflights are emitted in @layer cw-base while utilities are unlayered — a preflight can never override a utility, so .dark remap layers are silently inert

#1935
Closed glennmichael123 opened this 10 days ago ·
10 days ago

Crosswind emits preflights inside @layer cw-base but emits utility classes unlayered. Unlayered declarations beat layered ones regardless of specificity, so no preflight rule can ever override a utility class.

That matters here because the stx standards actively recommend preflights for exactly that job. In my app it left a ~60-rule hand-written dark-mode remap completely inert, silently, with no warning from any tool.

Found on 0.2.176 with @cwcss/crosswind as resolved by the framework.

Evidence

Walking the CSSOM on a rendered page and printing every rule that sets background-color for the sidebar:

applied bg : rgba(243, 243, 241, 0.48)      <- the light value, in dark mode
layers     : [ "cw-base" ]

layer=cw-base   rgba(24, 24, 27, 0.5)      .dark [data-postline-sidebar]
layer=cw-base   rgb(10, 10, 12)            .dark [data-postline-content-shell], .dark [data-postline-workspace]
layer=(none)    rgba(243, 243, 241, 0.48)  .bg-\[\#f3f3f1\]\/48

.dark [data-postline-sidebar] is one class plus one attribute (0,2,0); the utility is a single class (0,1,0). The more specific rule loses purely because it is in a layer and the utility is not.

Same result for the plain-utility remaps:

// dark mode, html.dark confirmed present
.bg-white     -> rgb(255, 255, 255)        // .dark .bg-white { … } never applies
.bg-white/95  -> rgba(255, 255, 255, 0.95)
.bg-panel     -> rgb(24, 24, 27)           // works — token-backed, flips via var(--panel)
.bg-panel-2   -> rgb(28, 28, 31)           // works

So the split is clean: token-backed utilities work (the custom property flips, the utility never needs overriding), and every attempt to remap a literal utility from preflights is dead.

Why it is easy to ship without noticing

The dead rules are indistinguishable from working ones at author time. They are syntactically valid, they appear in the served stylesheet, devtools shows them matching the element, and they are simply overruled. An app can carry a large dark-mode layer that has never once applied — mine did, across every page, and the pages still mostly looked right because the token-backed utilities carried the theme. The failures were confined to the handful of places using literal colours, which is precisely where nobody looks.

Concretely it cost me a sidebar rendering at 3.89:1 contrast in dark mode on every page (light panel, near-white text) with a correct-looking .dark [data-postline-sidebar] { background-color: rgba(24,24,27,0.5) } sitting in the config the whole time.

Suggested

Any of these would close the trap:

  1. Emit utilities into a layer too — e.g. @layer cw-base, cw-utilities; with utilities in cw-utilities. Preflights then lose by default, which is the correct precedence, and an author who wants to override reaches for @layer ordering deliberately. This is what Tailwind v4 does.
  2. Leave preflights unlayered as well, so normal specificity decides and .dark [data-x] beats .bg-x the way an author expects.
  3. If the layering is deliberate, document it at the preflights key and stop recommending preflights for utility remapping — and ideally warn at build time when a preflight selector would match an element that also carries a competing utility.

Docs angle for the standards

Rule 11.2 currently prescribes moving token blocks and the .dark .utility {} override layer into config/crosswind.ts preflights. The token half is right and works. The override half cannot work and should be replaced with the guidance that actually holds: define a custom property for the surface and use a token-backed utility, so there is nothing to override.

That is the fix I shipped — a --sidebar token that carries its own alpha in both themes, consumed as bg-sidebar — and it works because it goes through the mechanism that was never broken.

10 days ago

Duplicate of cwcss/crosswind#21, which is open and describes the same cause — preflights pinned to @layer cw-base while utilities ship unlayered. I filed this without checking; the app's own config/crosswind.ts already cites #21 in a comment directly above the code I was reading.

Leaving one observation here since it is about the standards rather than the engine, in case it is worth a docs change while #21 is open:

Rule 11.2 currently prescribes moving a .dark .utility {} override layer into config/crosswind.ts preflights. The token half of that advice works and is what carries dark mode correctly. The override half cannot work for as long as #21 stands, and it fails silently — the rules are valid, they appear in the served stylesheet, devtools shows them matching, and they are simply outranked.

Measured cost in one app: a sidebar rendering at 3.89:1 in dark mode on every page, with a correct-looking .dark [data-postline-sidebar] { background-color: rgba(24,24,27,0.5) } sitting in the config the whole time. The fix was to stop overriding and define the surface as a custom property consumed through a token-backed utility, so there is nothing to override.

10 days ago

Duplicate of cwcss/crosswind#21 — same cause, already open. I filed this without checking; the app's own config/crosswind.ts cites #21 in a comment directly above the code I was reading.

One observation worth keeping, since it is about the standards rather than the engine: rule 11.2 prescribes moving a .dark .utility {} override layer into preflights. The token half of that advice works and is what carries dark mode correctly; the override half cannot work while #21 stands, and it fails silently — valid rules, present in the stylesheet, shown matching in devtools, simply outranked.

Measured cost in one app: a sidebar at 3.89:1 contrast in dark mode on every page, with a correct-looking .dark [data-postline-sidebar] { background-color: rgba(24,24,27,0.5) } in the config the whole time. Fix was to stop overriding and define the surface as a custom property consumed via a token-backed utility.

Sign in to comment on this issue.