diff --git a/CHANGELOG.md b/CHANGELOG.md index c6be95ffc9a1..e8ffcf6c0414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Support `keyframes` in JS config file themes ([14594](https://github.com/tailwindlabs/tailwindcss/pull/14594)) + ### Fixed - Don’t crash when scanning a candidate equal to the configured prefix ([#14588](https://github.com/tailwindlabs/tailwindcss/pull/14588)) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index f702e5202292..1de9281f9a1d 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -5,6 +5,7 @@ import { withAlpha } from '../utilities' import { segment } from '../utils/segment' import { toKeyPath } from '../utils/to-key-path' import { applyConfigToTheme } from './apply-config-to-theme' +import { applyKeyframesToAst } from './apply-keyframes-to-ast' import { createCompatConfig } from './config/create-compat-config' import { resolveConfig } from './config/resolve-config' import type { UserConfig } from './config/types' @@ -206,6 +207,7 @@ export async function applyCompatibilityHooks({ // config would otherwise expand into namespaces like `background-color` which // core utilities already read from. applyConfigToTheme(designSystem, resolvedUserConfig) + applyKeyframesToAst(ast, resolvedUserConfig) registerThemeVariantOverrides(resolvedUserConfig, designSystem) registerScreensConfig(resolvedUserConfig, designSystem) diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-ast.test.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-ast.test.ts new file mode 100644 index 000000000000..dd95e0b3bfc5 --- /dev/null +++ b/packages/tailwindcss/src/compat/apply-keyframes-to-ast.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from 'vitest' +import { toCss, type AstNode } from '../ast' +import { buildDesignSystem } from '../design-system' +import { Theme } from '../theme' +import { applyKeyframesToAst } from './apply-keyframes-to-ast' +import { resolveConfig } from './config/resolve-config' + +test('Config values can be merged into the theme', () => { + let theme = new Theme() + let design = buildDesignSystem(theme) + + let ast: AstNode[] = [] + + let resolvedUserConfig = resolveConfig(design, [ + { + config: { + theme: { + keyframes: { + 'fade-in': { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + 'fade-out': { + '0%': { opacity: '1' }, + '100%': { opacity: '0' }, + }, + }, + }, + }, + base: '/root', + }, + ]) + applyKeyframesToAst(ast, resolvedUserConfig) + + expect(toCss(ast)).toMatchInlineSnapshot(` + "@keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + @keyframes fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/compat/apply-keyframes-to-ast.ts b/packages/tailwindcss/src/compat/apply-keyframes-to-ast.ts new file mode 100644 index 000000000000..2ca12ab50256 --- /dev/null +++ b/packages/tailwindcss/src/compat/apply-keyframes-to-ast.ts @@ -0,0 +1,11 @@ +import { rule, type AstNode } from '../ast' +import type { ResolvedConfig } from './config/types' +import { objectToAst } from './plugin-api' + +export function applyKeyframesToAst(ast: AstNode[], { theme }: ResolvedConfig) { + if ('keyframes' in theme) { + for (let [name, keyframe] of Object.entries(theme.keyframes)) { + ast.push(rule(`@keyframes ${name}`, objectToAst(keyframe as any))) + } + } +} diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index cbf4b29ece79..c0e04d576b09 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -67,6 +67,18 @@ describe('theme', async () => { } } } + @keyframes enter { + from { + opacity: var(--tw-enter-opacity, 1); + transform: translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0)); + } + } + @keyframes exit { + to { + opacity: var(--tw-exit-opacity, 1); + transform: translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0)); + } + } " `) }) diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index f6d8d91a57e1..c4d28de9a9e3 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -384,7 +384,7 @@ export function buildPluginApi( export type CssInJs = { [key: string]: string | string[] | CssInJs | CssInJs[] } -function objectToAst(rules: CssInJs | CssInJs[]): AstNode[] { +export function objectToAst(rules: CssInJs | CssInJs[]): AstNode[] { let ast: AstNode[] = [] rules = Array.isArray(rules) ? rules : [rules]