ReviewOS

stacks/ts-css

The class extractor misses a variant on an arbitrary property, so #36's fix is unreachable from a real build

#37
Closed glennmichael123 opened this 9 days ago ·
9 days ago

#36 fixed the generator: CSSGenerator.generate('motion-reduce:[animation:none]') now emits the right rule. But the class never reaches the generator in a real build, because whatever extracts class names from scanned content does not recognise variant:[property:value] as one token. The net effect for a consuming app is unchanged from #36 - the class still produces no CSS, silently.

Reproduction

A single element carrying five classes, served and then the generated stylesheet inspected:

<div class="h-5 w-[160px] bg-[color-mix(in_srgb,var(--text-3)_20%,transparent)] rounded-md [animation:pulse_1.4s_ease-in-out_infinite] motion-reduce:[animation:none]"></div>
classin generated sheet
w-[160px]yes
bg-[color-mix(in_srgb,…)]yes
[animation:pulse_1.4s_ease-in-out_infinite]yes
motion-reduce:[animation:none]no

Plain variants extract fine from the same page - that sheet contains active: and md: rules. So it is specifically a variant combined with an arbitrary property.

And the generator is fine when called directly, which is what makes this the extractor rather than a regression of #36:

const g = new CSSGenerator(defaultConfig)
g.generate('motion-reduce:[animation:none]')
// @media (prefers-reduced-motion: reduce) { .motion-reduce\:\[animation\:none\] { animation: none; } }

Measured on 0.3.15.

Why it matters

Same reason as #36: it fails silently. There is no error, no warning and no rule, so the only way to notice is to read the generated stylesheet and compare it against the markup.

In our case the class was motion-reduce:[animation:none] on a skeleton loader, which means a reduced-motion guard that has never once worked. That is the failure mode worth worrying about - the cases people reach for an arbitrary property with a variant tend to be the accessibility and edge-state ones, which are exactly the ones nobody looks at.

Suggested fix

Wherever the scanner tokenises candidate class names, the bracket span needs to be treated as opaque so the : inside [animation:none] is not read as a variant separator or as the end of the token. The generator already does this correctly after #36, so the two just need to agree on what a class name is.

Worth a test that goes through the scan path rather than calling generate() directly, since #36's coverage would not have caught this.

Sign in to comment on this issue.