Skip to content

Commit 6d43a8b

Browse files
authored
Preserve explicit transition duration and timing function when overriding transition property (#14490)
This PR changes the behavior of the `transition-{property}` utilities to respect any explicit timing function or duration set by the user using the `ease-*` and `duration-*` utilities. Say you have this HTML: ```html <div class="transition-colors duration-500 ease-out lg:transition-all"> ``` Currently, the `transition-duration` and `transition-timing-functions` will be reset to their default values at the `lg` breakpoint even though you've provided explicit values for them. After this PR is merged, those values will be preserved at the `lg` breakpoint. This PR also adds `duration-initial` and `ease-initial` utilities to "unset" explicit duration/timing-function values so that the defaults from classes like `transition-all` will kick in, without having to specify their explicit values. --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent 222a93c commit 6d43a8b

File tree

5 files changed

+134
-54
lines changed

5 files changed

+134
-54
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232
- Preserve explicit `leading-*`, `tracking-*`, and `font-{weight}` value when overriding font-size ([#14403](https://github.com/tailwindlabs/tailwindcss/pull/14403))
3333
- Disallow negative bare values in core utilities and variants ([#14453](https://github.com/tailwindlabs/tailwindcss/pull/14453))
3434
- Preserve explicit shadow color when overriding shadow size ([#14458](https://github.com/tailwindlabs/tailwindcss/pull/14458))
35+
- Preserve explicit transition duration and timing function when overriding transition property ([#14490](https://github.com/tailwindlabs/tailwindcss/pull/14490))
3536

3637
## [4.0.0-alpha.24] - 2024-09-11
3738

packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1908,6 +1908,8 @@ exports[`getClassList 1`] = `
19081908
"duration-500",
19091909
"duration-700",
19101910
"duration-75",
1911+
"duration-initial",
1912+
"ease-initial",
19111913
"end-0.5",
19121914
"end-1",
19131915
"end-3",

packages/tailwindcss/src/utilities.test.ts

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12898,47 +12898,47 @@ test('transition', async () => {
1289812898
1289912899
.transition {
1290012900
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, -webkit-backdrop-filter, backdrop-filter;
12901-
transition-timing-function: var(--default-transition-timing-function, ease);
12902-
transition-duration: var(--default-transition-duration, .1s);
12901+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
12902+
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
1290312903
}
1290412904
1290512905
.transition-\\[--value\\] {
1290612906
transition-property: var(--value);
12907-
transition-timing-function: var(--default-transition-timing-function, ease);
12908-
transition-duration: var(--default-transition-duration, .1s);
12907+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
12908+
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
1290912909
}
1291012910
1291112911
.transition-all {
1291212912
transition-property: all;
12913-
transition-timing-function: var(--default-transition-timing-function, ease);
12914-
transition-duration: var(--default-transition-duration, .1s);
12913+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
12914+
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
1291512915
}
1291612916
1291712917
.transition-colors {
1291812918
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
12919-
transition-timing-function: var(--default-transition-timing-function, ease);
12920-
transition-duration: var(--default-transition-duration, .1s);
12919+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
12920+
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
1292112921
}
1292212922
1292312923
.transition-opacity {
1292412924
transition-property: opacity;
12925-
transition-timing-function: var(--default-transition-timing-function, ease);
12926-
transition-duration: var(--default-transition-duration, .1s);
12925+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
12926+
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
1292712927
transition-property: var(--transition-property-opacity, opacity);
12928-
transition-timing-function: var(--default-transition-timing-function, ease);
12929-
transition-duration: var(--default-transition-duration, .1s);
12928+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
12929+
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
1293012930
}
1293112931
1293212932
.transition-shadow {
1293312933
transition-property: box-shadow;
12934-
transition-timing-function: var(--default-transition-timing-function, ease);
12935-
transition-duration: var(--default-transition-duration, .1s);
12934+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
12935+
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
1293612936
}
1293712937
1293812938
.transition-transform {
1293912939
transition-property: transform, translate, scale, rotate;
12940-
transition-timing-function: var(--default-transition-timing-function, ease);
12941-
transition-duration: var(--default-transition-duration, .1s);
12940+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
12941+
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
1294212942
}
1294312943
1294412944
.transition-none {
@@ -12965,20 +12965,20 @@ test('transition', async () => {
1296512965
1296612966
.transition {
1296712967
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, -webkit-backdrop-filter, backdrop-filter;
12968-
transition-duration: .1s;
12969-
transition-timing-function: ease;
12968+
transition-timing-function: var(--tw-ease, ease);
12969+
transition-duration: var(--tw-duration, .1s);
1297012970
}
1297112971
1297212972
.transition-all {
1297312973
transition-property: all;
12974-
transition-duration: .1s;
12975-
transition-timing-function: ease;
12974+
transition-timing-function: var(--tw-ease, ease);
12975+
transition-duration: var(--tw-duration, .1s);
1297612976
}
1297712977
1297812978
.transition-colors {
1297912979
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
12980-
transition-duration: .1s;
12981-
transition-timing-function: ease;
12980+
transition-timing-function: var(--tw-ease, ease);
12981+
transition-duration: var(--tw-duration, .1s);
1298212982
}"
1298312983
`)
1298412984

@@ -12992,8 +12992,8 @@ test('transition', async () => {
1299212992
).toMatchInlineSnapshot(`
1299312993
".transition-all {
1299412994
transition-property: all;
12995-
transition-duration: 0s;
12996-
transition-timing-function: ease;
12995+
transition-timing-function: var(--tw-ease, ease);
12996+
transition-duration: var(--tw-duration, 0s);
1299712997
}"
1299812998
`)
1299912999

@@ -13047,15 +13047,31 @@ test('delay', async () => {
1304713047
test('duration', async () => {
1304813048
expect(await run(['duration-123', 'duration-200', 'duration-[300ms]'])).toMatchInlineSnapshot(`
1304913049
".duration-123 {
13050+
--tw-duration: .123s;
1305013051
transition-duration: .123s;
1305113052
}
1305213053
1305313054
.duration-200 {
13055+
--tw-duration: .2s;
1305413056
transition-duration: .2s;
1305513057
}
1305613058
1305713059
.duration-\\[300ms\\] {
13060+
--tw-duration: .3s;
1305813061
transition-duration: .3s;
13062+
}
13063+
13064+
@supports (-moz-orient: inline) {
13065+
@layer base {
13066+
*, :before, :after, ::backdrop {
13067+
--tw-duration: initial;
13068+
}
13069+
}
13070+
}
13071+
13072+
@property --tw-duration {
13073+
syntax: "*";
13074+
inherits: false
1305913075
}"
1306013076
`)
1306113077
expect(
@@ -13090,15 +13106,31 @@ test('ease', async () => {
1309013106
}
1309113107
1309213108
.ease-\\[--value\\] {
13109+
--tw-ease: var(--value);
1309313110
transition-timing-function: var(--value);
1309413111
}
1309513112
1309613113
.ease-in {
13114+
--tw-ease: var(--transition-timing-function-in, cubic-bezier(.4, 0, 1, 1));
1309713115
transition-timing-function: var(--transition-timing-function-in, cubic-bezier(.4, 0, 1, 1));
1309813116
}
1309913117
1310013118
.ease-out {
13119+
--tw-ease: var(--transition-timing-function-out, cubic-bezier(0, 0, .2, 1));
1310113120
transition-timing-function: var(--transition-timing-function-out, cubic-bezier(0, 0, .2, 1));
13121+
}
13122+
13123+
@supports (-moz-orient: inline) {
13124+
@layer base {
13125+
*, :before, :after, ::backdrop {
13126+
--tw-ease: initial;
13127+
}
13128+
}
13129+
}
13130+
13131+
@property --tw-ease {
13132+
syntax: "*";
13133+
inherits: false
1310213134
}"
1310313135
`)
1310413136
expect(

packages/tailwindcss/src/utilities.ts

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3701,9 +3701,8 @@ export function createUtilities(theme: Theme) {
37013701
}
37023702

37033703
{
3704-
let defaultTimingFunction =
3705-
theme.resolve(null, ['--default-transition-timing-function']) ?? 'ease'
3706-
let defaultDuration = theme.resolve(null, ['--default-transition-duration']) ?? '0s'
3704+
let defaultTimingFunction = `var(--tw-ease, ${theme.resolve(null, ['--default-transition-timing-function']) ?? 'ease'})`
3705+
let defaultDuration = `var(--tw-duration, ${theme.resolve(null, ['--default-transition-duration']) ?? '0s'})`
37073706

37083707
staticUtility('transition-none', [['transition-property', 'none']])
37093708
staticUtility('transition-all', [
@@ -3755,37 +3754,49 @@ export function createUtilities(theme: Theme) {
37553754
handle: (value) => [decl('transition-delay', value)],
37563755
})
37573756

3758-
utilities.functional('duration', (candidate) => {
3759-
// This utility doesn't support negative values.
3760-
if (candidate.negative) return
3757+
{
3758+
let transitionDurationProperty = () => {
3759+
return atRoot([property('--tw-duration')])
3760+
}
37613761

3762-
// This utility doesn't support modifiers.
3763-
if (candidate.modifier) return
3762+
staticUtility('duration-initial', [transitionDurationProperty, ['--tw-duration', 'initial']])
37643763

3765-
// This utility doesn't support `DEFAULT` values.
3766-
if (!candidate.value) return
3764+
utilities.functional('duration', (candidate) => {
3765+
// This utility doesn't support negative values.
3766+
if (candidate.negative) return
37673767

3768-
// Find the actual CSS value that the candidate value maps to.
3769-
let value: string | null = null
3768+
// This utility doesn't support modifiers.
3769+
if (candidate.modifier) return
37703770

3771-
if (candidate.value.kind === 'arbitrary') {
3772-
value = candidate.value.value
3773-
} else {
3774-
value = theme.resolve(candidate.value.fraction ?? candidate.value.value, [
3775-
'--transition-duration',
3776-
])
3771+
// This utility doesn't support `DEFAULT` values.
3772+
if (!candidate.value) return
37773773

3778-
if (value === null && isPositiveInteger(candidate.value.value)) {
3779-
value = `${candidate.value.value}ms`
3774+
// Find the actual CSS value that the candidate value maps to.
3775+
let value: string | null = null
3776+
3777+
if (candidate.value.kind === 'arbitrary') {
3778+
value = candidate.value.value
3779+
} else {
3780+
value = theme.resolve(candidate.value.fraction ?? candidate.value.value, [
3781+
'--transition-duration',
3782+
])
3783+
3784+
if (value === null && isPositiveInteger(candidate.value.value)) {
3785+
value = `${candidate.value.value}ms`
3786+
}
37803787
}
3781-
}
37823788

3783-
// If the candidate value (like the `sm` in `max-w-sm`) doesn't resolve to
3784-
// an actual value, don't generate any rules.
3785-
if (value === null) return
3789+
// If the candidate value (like the `sm` in `max-w-sm`) doesn't resolve to
3790+
// an actual value, don't generate any rules.
3791+
if (value === null) return
37863792

3787-
return [decl('transition-duration', value)]
3788-
})
3793+
return [
3794+
transitionDurationProperty(),
3795+
decl('--tw-duration', value),
3796+
decl('transition-duration', value),
3797+
]
3798+
})
3799+
}
37893800

37903801
suggest('delay', () => [
37913802
{
@@ -3802,10 +3813,22 @@ export function createUtilities(theme: Theme) {
38023813
])
38033814
}
38043815

3805-
functionalUtility('ease', {
3806-
themeKeys: ['--transition-timing-function'],
3807-
handle: (value) => [decl('transition-timing-function', value)],
3808-
})
3816+
{
3817+
let transitionTimingFunctionProperty = () => {
3818+
return atRoot([property('--tw-ease')])
3819+
}
3820+
3821+
staticUtility('ease-initial', [transitionTimingFunctionProperty, ['--tw-ease', 'initial']])
3822+
3823+
functionalUtility('ease', {
3824+
themeKeys: ['--transition-timing-function'],
3825+
handle: (value) => [
3826+
transitionTimingFunctionProperty(),
3827+
decl('--tw-ease', value),
3828+
decl('transition-timing-function', value),
3829+
],
3830+
})
3831+
}
38093832

38103833
staticUtility('will-change-auto', [['will-change', 'auto']])
38113834
staticUtility('will-change-scroll', [['will-change', 'scroll-position']])

packages/tailwindcss/tests/ui.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,28 @@ test('explicit font-weight utilities are respected when overriding font-size', a
604604
expect(await getPropertyValue('#z', 'font-weight')).toEqual('900')
605605
})
606606

607+
test('explicit duration and ease utilities are respected when overriding transition-property', async ({
608+
page,
609+
}) => {
610+
let { getPropertyValue } = await render(
611+
page,
612+
html`
613+
<div
614+
id="x"
615+
class="ease-[linear] duration-500 transition-[opacity] hover:transition-[background-color]"
616+
>
617+
Hello world
618+
</div>
619+
`,
620+
)
621+
622+
expect(await getPropertyValue('#x', 'transition-timing-function')).toEqual('linear')
623+
expect(await getPropertyValue('#x', 'transition-duration')).toEqual('0.5s')
624+
await page.locator('#x').hover()
625+
expect(await getPropertyValue('#x', 'transition-timing-function')).toEqual('linear')
626+
expect(await getPropertyValue('#x', 'transition-duration')).toEqual('0.5s')
627+
})
628+
607629
// ---
608630

609631
const preflight = fs.readFileSync(path.resolve(__dirname, '..', 'preflight.css'), 'utf-8')

0 commit comments

Comments
 (0)