diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fec361f5959..14ff52f444fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417)) - Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419)) - Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))` → `selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420)) +- Don't crash when a JS config explicitly sets `theme.keyframes` to `null` ([#20425](https://github.com/tailwindlabs/tailwindcss/pull/20425)) ## [4.3.3] - 2026-07-16 diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts index aed761fcec03..66161cfb8650 100644 --- a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.test.ts @@ -55,6 +55,53 @@ test('keyframes can be merged into the theme', () => { `) }) +test('setting `theme.keyframes` to `null` does not throw and clears keyframes', () => { + let theme = new Theme() + let design = buildDesignSystem(theme) + + let { resolvedConfig } = resolveConfig(design, [ + { + config: { + theme: { + keyframes: null, + }, + }, + base: '/root', + reference: false, + src: undefined, + }, + ]) + + expect(() => applyKeyframesToTheme(design, resolvedConfig)).not.toThrow() + expect(toCss(design.theme.getKeyframes())).toEqual('') +}) + +test('setting `theme.extend.keyframes` to `null` does not throw', () => { + // Unlike a top-level `theme.keyframes: null`, this is a no-op: `deepMerge` + // skips `null`/`undefined` sources entirely, so the resolved value here is + // `{}`, not `null`. This test guards against that assumption changing. + let theme = new Theme() + let design = buildDesignSystem(theme) + + let { resolvedConfig } = resolveConfig(design, [ + { + config: { + theme: { + extend: { + keyframes: null, + }, + }, + }, + base: '/root', + reference: false, + src: undefined, + }, + ]) + + expect(() => applyKeyframesToTheme(design, resolvedConfig)).not.toThrow() + expect(toCss(design.theme.getKeyframes())).toEqual('') +}) + test('will append to the default keyframes with new keyframes', () => { let theme = new Theme() let design = buildDesignSystem(theme) diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts index d5855545f1c6..620c7b3fa2ae 100644 --- a/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-keyframes-to-theme.ts @@ -14,7 +14,7 @@ export function applyKeyframesToTheme( export function keyframesToRules(resolvedConfig: Pick): AtRule[] { let rules: AtRule[] = [] - if ('keyframes' in resolvedConfig.theme) { + if (resolvedConfig.theme.keyframes) { for (let [name, keyframe] of Object.entries(resolvedConfig.theme.keyframes)) { rules.push(atRule('@keyframes', name, objectToAst(keyframe as any))) }