ReviewOS

stacks/ts-css

theme.extend.keyframes / theme.extend.animation are silently ignored — animate-<name> generates nothing

#22
Open glennmichael123 opened this 9 days ago ·
9 days ago

There is no registration point for a custom @keyframes or a named animation. theme.extend.keyframes and theme.extend.animation are accepted by the deep-merge and then never read, so animate-<name> generates nothing — silently.

Repro

import { CSSGenerator, defaultConfig } from '@cwcss/crosswind'

const g = new CSSGenerator({
  ...defaultConfig,
  theme: {
    ...defaultConfig.theme,
    extend: {
      ...defaultConfig.theme?.extend,
      keyframes: { 'reveal-up': { from: { opacity: '0' }, to: { opacity: '1' } } },
      animation: { reveal: 'reveal-up 1s linear both' },
    },
  },
})

g.generate('animate-reveal')
g.generate('animate-spin')   // control
const css = g.toCSS()

css.includes('.animate-reveal')   // false
css.includes('reveal-up')         // false  — no rule, no @keyframes
css.includes('.animate-spin')     // true   } control proves the method
/@keyframes\s+spin/.test(css)     // true   } finds what does exist

The control matters: the same generator call emits .animate-spin and its @keyframes spin block, so this isn't a harness mistake — reveal-up is genuinely dropped.

Cause

dist/chunk-jbn8cyfb.js:453 emits keyframes only from a frozen built-in map:

for (let W of this.usedKeyframes) { let H = gX[W]; if (H) J.push(...) }

gX contains exactly four entries — spin, ping, pulse, bounce. There is no path from theme config into it.

dist/types.d.ts:51-60 confirms the surface: Theme { colors; spacing; fontSize; fontFamily; lineHeight?; screens; borderRadius; boxShadow; extend? } — no keyframes, no animation. rg -n 'keyframes' dist/*.d.ts returns 0 hits (validated against rg -n 'shortcuts' dist/*.d.ts → 2 hits, so the search works).

CustomRule (types.d.ts:151) returns Record<string, string> — declarations only — so a custom rule can't emit an at-rule either.

Why it matters

The only way to ship a custom animation is a raw preflight string containing both the @keyframes block and a hand-written class. Ours does exactly that twice, and those hand-written classes are then invisible to the extractor, can't be purged, and don't compose with variants the way a real utility would.

One good sign: the variant half already works — motion-reduce:animate-none generates correctly. So motion-reduce:animate-reveal would compose properly the moment animate-reveal can be registered. It's only the registry that's missing.

Ask

Read theme.extend.keyframes and theme.extend.animation the way the rest of theme.extend is read, merging into the built-in map rather than replacing it, and emit @keyframes for any animation actually used.

If custom keyframes are intentionally out of scope, a build-time warning on an unresolvable animate-* would still be a large improvement over silence — right now a typo and an unregistered animation are indistinguishable, and both produce a page that just doesn't animate.

Environment

crosswind 0.2.15, consumed through @stacksjs/stx 0.2.153, Bun 1.3.1.

Sign in to comment on this issue.