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.
There is no registration point for a custom
@keyframesor a named animation.theme.extend.keyframesandtheme.extend.animationare accepted by the deep-merge and then never read, soanimate-<name>generates nothing — silently.Repro
The control matters: the same generator call emits
.animate-spinand its@keyframes spinblock, so this isn't a harness mistake —reveal-upis genuinely dropped.Cause
dist/chunk-jbn8cyfb.js:453emits keyframes only from a frozen built-in map:gXcontains exactly four entries —spin,ping,pulse,bounce. There is no path from theme config into it.dist/types.d.ts:51-60confirms the surface:Theme { colors; spacing; fontSize; fontFamily; lineHeight?; screens; borderRadius; boxShadow; extend? }— nokeyframes, noanimation.rg -n 'keyframes' dist/*.d.tsreturns 0 hits (validated againstrg -n 'shortcuts' dist/*.d.ts→ 2 hits, so the search works).CustomRule(types.d.ts:151) returnsRecord<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
@keyframesblock 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-nonegenerates correctly. Somotion-reduce:animate-revealwould compose properly the momentanimate-revealcan be registered. It's only the registry that's missing.Ask
Read
theme.extend.keyframesandtheme.extend.animationthe way the rest oftheme.extendis read, merging into the built-in map rather than replacing it, and emit@keyframesfor 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.