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) // worksSo 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:
- Emit utilities into a layer too — e.g.
@layer cw-base, cw-utilities;with utilities incw-utilities. Preflights then lose by default, which is the correct precedence, and an author who wants to override reaches for@layerordering deliberately. This is what Tailwind v4 does. - Leave preflights unlayered as well, so normal specificity decides and
.dark [data-x]beats.bg-xthe way an author expects. - If the layering is deliberate, document it at the
preflightskey 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.