Skip to content

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

Description

@glennmichael123

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions