Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support keyframes in JS theme config #14594

Merged
merged 2 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions packages/tailwindcss/src/compat/apply-compat-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions packages/tailwindcss/src/compat/apply-keyframes-to-ast.test.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
"
`)
})
11 changes: 11 additions & 0 deletions packages/tailwindcss/src/compat/apply-keyframes-to-ast.ts
Original file line number Diff line number Diff line change
@@ -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)))
}
}
}
12 changes: 12 additions & 0 deletions packages/tailwindcss/src/compat/plugin-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Comment on lines +70 to +81
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh oh wait what.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting, in v3, plugins require an addBase to add the keyframes but if it was part of the JS config theme, it would be added automatically. That's lovely:
https://play.tailwindcss.com/YQLQchsWWW?file=config

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In v3 the way this worked was keyframes were not emitted until used. So you had to do @keyframes …: theme('keyframes.whatever`) in your utility function. And we handle that already.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we do in addUtilities at least. I don't think we do in matchUtilities which we should I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or at least I'm like 95% sure it works that way. Maybe we just de-duped them…

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh gotch you're right they're only added when used! If you use addBase and use it with an animate-* variable, it is emitted twice in v3 (at the end of the stylesheet).

Screenshot 2024-10-04 at 12 56 42

I guess it's fine if we always emit keyframes for compat now (we do this for v4 keyframes too!). However unsure if we should then guard against this case. Maybe it's ok if the keyframes are emitted twice here since it does no harm? WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it's probably fine — would be nice to dedupe them but that can be an optimization done later. They should still be de-duped when minifying with lightingcss.

"
`)
})
Expand Down
2 changes: 1 addition & 1 deletion packages/tailwindcss/src/compat/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down