Skip to content

Commit 8dc343d

Browse files
authored
Migrate theme(…) calls to var(…) or the modern theme(…) syntax (tailwindlabs#14664)
This PR adds a codemod to convert `theme(…)` calls to `var(…)` calls. If we can't safely do this, then we try to convert the `theme(…)` syntax (dot notation) to the modern `theme(…)` syntax (with CSS variable-like syntax). ### Let's look at some examples: **Simple example:** Input: ```html <div class="bg-[theme(colors.red.500)]"></div> ``` Output: ```html <div class="bg-[var(--color-red-500)]"></div> ``` --- **With fallback:** Input: ```html <div class="bg-[theme(colors.red.500,theme(colors.blue.500))]"></div> ``` Output: ```html <div class="bg-[var(--color-red-500,var(--color-blue-500))]"></div> ``` --- **With modifiers:** Input: ```html <div class="bg-[theme(colors.red.500/75%)]"></div> ``` Output: ```html <div class="bg-[var(--color-red-500)]/75"></div> ``` We can special case this, because if you are using that modifier syntax we _assume_ it's being used in a `theme(…)` call referencing a color. This means that we can also convert it to a modifier on the actual candidate. --- **With modifier, if a modifier is already present:** Input: ```html <div class="bg-[theme(colors.red.500/75%)]/50"></div> ``` Output: ```html <div class="bg-[theme(--color-red-500/75%)]/50"></div> ``` In this case we can't use the `var(…)` syntax because that requires us to move the opacity modifier to the candidate itself. In this case we could use math to figure out the expected modifier, but that might be too confusing. Instead, we convert to the modern `theme(…)` syntax. --- **Multiple `theme(…)` calls with modifiers:** Input: ```html <div class="bg-[theme(colors.red.500/75%,theme(colors.blue.500/50%))]"></div> ``` Output: ```html <div class="bg-[theme(--color-red-500/75%,theme(--color-blue-500/50%))]"></div> ``` In this case we can't convert to `var(…)` syntax because then we lose the opacity modifier. We also can't move the opacity modifier to the candidate itself e.g.: `/50` because we have 2 different variables to worry about. In this situation we convert to the modern `theme(…)` syntax itself. --- **Inside variants:** Input: ```html <div class="max-[theme(spacing.20)]:flex"></div> ``` Output: ```html <div class="max-[theme(--spacing-20)]:flex"></div> ``` Unfortunately we can't convert to `var(…)` syntax reliably because in some cases (like the one above) the value will be used inside of an `@media (…)` query and CSS doesn't support that at the time of writing this PR. So to be safe, we will not try to convert `theme(…)` to `var(…)` in variants, but we will only upgrade the `theme(…)` call itself to modern syntax.
1 parent bf17991 commit 8dc343d

File tree

6 files changed

+403
-3
lines changed

6 files changed

+403
-3
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- _Upgrade (experimental)_: Migrate `theme(…)` calls in classes to `var(…)` or to the modern `theme(…)` syntax ([#14664](https://github.com/tailwindlabs/tailwindcss/pull/14664))
13+
1014
### Fixed
1115

1216
- Ensure `theme` values defined outside of `extend` in JS configuration files overwrite all existing values for that namespace ([#14672](https://github.com/tailwindlabs/tailwindcss/pull/14672))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
2+
import { expect, test } from 'vitest'
3+
import { themeToVar } from './theme-to-var'
4+
5+
test.each([
6+
// Keep candidates that don't contain `theme(…)` or `theme(…, …)`
7+
['[color:red]', '[color:red]'],
8+
9+
// Convert to `var(…)` if we can resolve the path
10+
['[color:theme(colors.red.500)]', '[color:var(--color-red-500)]'], // Arbitrary property
11+
['[color:theme(colors.red.500)]/50', '[color:var(--color-red-500)]/50'], // Arbitrary property + modifier
12+
['bg-[theme(colors.red.500)]', 'bg-[var(--color-red-500)]'], // Arbitrary value
13+
['bg-[size:theme(spacing.4)]', 'bg-[size:var(--spacing-4)]'], // Arbitrary value + data type hint
14+
15+
// Convert to `var(…)` if we can resolve the path, but keep fallback values
16+
['bg-[theme(colors.red.500,red)]', 'bg-[var(--color-red-500,_red)]'],
17+
18+
// Keep `theme(…)` if we can't resolve the path
19+
['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'],
20+
21+
// Keep `theme(…)` if we can't resolve the path, but still try to convert the
22+
// fallback value.
23+
[
24+
'bg-[theme(colors.foo.1000,theme(colors.red.500))]',
25+
'bg-[theme(colors.foo.1000,var(--color-red-500))]',
26+
],
27+
28+
// Use `theme(…)` (deeply nested) inside of a `calc(…)` function
29+
['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--font-size-xs)_*_2)]'],
30+
31+
// Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)`
32+
// - Can't convert to `var(…)` because that would lose the modifier.
33+
// - Can't convert to a candidate modifier because there are multiple
34+
// `theme(…)` calls.
35+
//
36+
// If we really want to, we can make a fancy migration that tries to move it
37+
// to a candidate modifier _if_ all `theme(…)` calls use the same modifier.
38+
[
39+
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]',
40+
'[color:theme(--color-red-500/50,_theme(--color-blue-500/50))]',
41+
],
42+
[
43+
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]/50',
44+
'[color:theme(--color-red-500/50,_theme(--color-blue-500/50))]/50',
45+
],
46+
47+
// Convert the `theme(…)`, but try to move the inline modifier (e.g. `50%`),
48+
// to a candidate modifier.
49+
// Arbitrary property, with simple percentage modifier
50+
['[color:theme(colors.red.500/75%)]', '[color:var(--color-red-500)]/75'],
51+
52+
// Arbitrary property, with numbers (0-1) without a unit
53+
['[color:theme(colors.red.500/.12)]', '[color:var(--color-red-500)]/12'],
54+
['[color:theme(colors.red.500/0.12)]', '[color:var(--color-red-500)]/12'],
55+
56+
// Arbitrary property, with more complex modifier (we only allow whole numbers
57+
// as bare modifiers). Convert the complex numbers to arbitrary values instead.
58+
['[color:theme(colors.red.500/12.34%)]', '[color:var(--color-red-500)]/[12.34%]'],
59+
['[color:theme(colors.red.500/var(--opacity))]', '[color:var(--color-red-500)]/[var(--opacity)]'],
60+
['[color:theme(colors.red.500/.12345)]', '[color:var(--color-red-500)]/[12.345]'],
61+
['[color:theme(colors.red.500/50.25%)]', '[color:var(--color-red-500)]/[50.25%]'],
62+
63+
// Arbitrary value
64+
['bg-[theme(colors.red.500/75%)]', 'bg-[var(--color-red-500)]/75'],
65+
['bg-[theme(colors.red.500/12.34%)]', 'bg-[var(--color-red-500)]/[12.34%]'],
66+
67+
// Arbitrary property that already contains a modifier
68+
['[color:theme(colors.red.500/50%)]/50', '[color:theme(--color-red-500/50%)]/50'],
69+
70+
// Arbitrary value, where the candidate already contains a modifier
71+
// This should still migrate the `theme(…)` syntax to the modern syntax.
72+
['bg-[theme(colors.red.500/50%)]/50', 'bg-[theme(--color-red-500/50%)]/50'],
73+
74+
// Variants, we can't use `var(…)` especially inside of `@media(…)`. We can
75+
// still upgrade the `theme(…)` to the modern syntax.
76+
['max-[theme(spacing.4)]:flex', 'max-[theme(--spacing-4)]:flex'],
77+
78+
// This test in itself doesn't make much sense. But we need to make sure
79+
// that this doesn't end up as the modifier in the candidate itself.
80+
['max-[theme(spacing.4/50)]:flex', 'max-[theme(--spacing-4/50)]:flex'],
81+
82+
// `theme(…)` calls valid in v3, but not in v4 should still be converted.
83+
['[--foo:theme(fontWeight.semibold)]', '[--foo:theme(fontWeight.semibold)]'],
84+
85+
// Invalid cases
86+
['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'],
87+
['[--foo:theme(colors.red.500/50/50)]/50', '[--foo:theme(colors.red.500/50/50)]/50'],
88+
89+
// Partially invalid cases
90+
[
91+
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]',
92+
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]',
93+
],
94+
[
95+
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]/50',
96+
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]/50',
97+
],
98+
])('%s => %s', async (candidate, result) => {
99+
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
100+
base: __dirname,
101+
})
102+
103+
expect(themeToVar(designSystem, {}, candidate)).toEqual(result)
104+
})

0 commit comments

Comments
 (0)