ReviewOS

stacks/ts-css

A variant on an arbitrary property emits nothing: hover:[border-color:red] and focus:/md: equivalents produce no CSS, silently

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

A variant prefix on an arbitrary property emits nothing. No error, no warning, no rule — the class simply produces no CSS, so the only way to notice is to read the generated stylesheet or see the effect missing in a browser.

The bare form works, and the same variant works on a normal utility. It is only the combination that is lost.

Reproduction

import { CSSGenerator, defaultConfig } from '@stacksjs/ts-css/engine'

for (const cls of [
  'hover:[border-color:red]',
  'focus:[outline:2px_solid_red]',
  'md:[grid-template-columns:1fr_2fr]',
  '[color:inherit]',
  'hover:border-[red]',
]) {
  const g = new CSSGenerator(defaultConfig)
  g.generate(cls)
  console.log(g.toCSS(false, false) ? 'EMIT' : 'NONE', cls)
}
NONE  hover:[border-color:red]
NONE  focus:[outline:2px_solid_red]
NONE  md:[grid-template-columns:1fr_2fr]
EMIT  [color:inherit]
EMIT  hover:border-[red]

Not specific to hover, and not specific to colours — focus: and the md: breakpoint fail the same way, and so does any property I tried. Measured identically on 0.3.6 and 0.3.11, so the Tailwind v4 alignment batch did not change it.

Tailwind supports this (hover:[mask-type:luminance]), so the arbitrary-property form reads as a complete escape hatch when it is only half of one.

Why it bites

The arbitrary-property form is the reliable way to express something the utility vocabulary cannot reach, so it is exactly what you reach for when a utility has already failed you — and then it silently fails too. In our case hover:[border-color:color-mix(…)] was the natural spelling for a card's hover border; it compiled to nothing, and the element kept its transition while losing the border it transitions to. Nothing in the build, the tests or the type checker said so.

hover:border-[color-mix(…)] turned out to work and is what we shipped, but that is a workaround found by reading generated CSS, not something the API suggests.

Suggested fix

arbitraryPropertyRule in packages/toolkit/src/engine/rules-advanced.ts gates on parsed.base.startsWith('['). With a variant prefix present, base appears to still carry the prefix (hover:[border-color:red]), so the check fails and the rule returns undefined. Matching against the post-variant portion of the class — the same string the bare form sees — would likely be enough.

If supporting it is not wanted, emitting a warning for a class that produces no CSS would at least make the failure visible, since this whole class of bug is only discoverable by inspecting output.

9 days ago

Fixed in 0.3.14 by fix: make arbitrary properties work with variants and keep their names literal. Verified against the published 0.3.15 artifact from npm, generating each class through CSSGenerator and reading the output:

EMIT  hover:[border-color:color-mix(in_srgb,var(--accent)_45%,var(--border))]
      .hover\:\[border-color\:…\]:hover { border-color: color-mix(in srgb,var(--accent) 45%,var(--border)); }
EMIT  focus:[outline:2px_solid_var(--accent)]
      .focus\:\[outline\:…\]:focus { outline: 2px solid var(--accent); }
EMIT  md:[grid-template-columns:1fr_2fr]
      @media (min-width: 768px) { .md\:\[grid-template-columns\:1fr_2fr\] { grid-template-columns: 1fr 2fr; } }

All three emitted nothing on 0.3.6 through 0.3.13. The breakpoint case lands inside its @media correctly rather than at top level, which is the part I would have expected to need a second pass. Thanks for the quick turnaround.

Closing as fixed.

9 days ago

Correction to my close above, with apologies: the generator fix is real, but it is not reachable from a real build. The class extractor does not recognise variant:[property:value] as one token, so these classes never reach the generator when scanning a page, and the net effect for a consuming app is unchanged - no CSS, silently.

I verified the generator in isolation, which is why I missed it. On a served page carrying motion-reduce:[animation:none], the sheet contains w-[160px], bg-[color-mix(…)] and the bare [animation:…] from the same element, plus active: and md: rules from elsewhere, but not the one with the variant.

Filed separately as #37 so the extractor side has its own issue. Leaving this closed since the generator part is genuinely fixed.

Sign in to comment on this issue.