Skip to content

Commit 6c85327

Browse files
Convert opacity keys to percentage values (#15067)
This PR improves compatibility for named `opacity` theme values. One of the changes in v4 is that we use the CSS `color-mix()` function to apply opacity to colors. This, however, is limited to percentage values only: ```css color: color-mix(in oklch, var(--color-red-500) 50%, transparent); /* ^^^ */ /* This needs to be a percentage value */ ``` In v3, however, it was common to specify custom opacity values as decimal numbers. That's also what we did in our default config: https://github.com/tailwindlabs/tailwindcss/blob/6069a811871c58a9b202fbb3a6f13774c57278c0/stubs/config.full.js#L703-L725 This PR improves interop with these values by: 1. Converting decimal numbers in the range of `[0, 1]` to their percentage value equivalent when using the interop layer. 2. Adjusts the codemod that migrates `opacity` theme keys to Tailwind v4 theme variables to include the same conversion. 3. Furthermore, due to the added support of named opacity modifers, we can also drop theme keys that would now be the default. For example the following config would not be necessary in v4 as the opacity modifier would accept the value `50` by default: ```js module.exports = { theme: { opacity: { 50: 0.5 } } } ``` ## Test Plan Added a new integration test for the codemod and a unit test for the interop layer. I also re-ran the codemod on the Commit template and it's now working as expected (left is v3, right is v4): <img width="2560" alt="Screenshot 2024-11-21 at 17 25 32" src="https://github.com/user-attachments/assets/f0c87243-ca80-4c39-ae5e-c1ab48fbe614">
1 parent 5d2c64e commit 6c85327

File tree

5 files changed

+203
-1
lines changed

5 files changed

+203
-1
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- _Upgrade (experimental)_: Drop unnecessary `opacity` theme values when migrating to CSS ([#15067](https://github.com/tailwindlabs/tailwindcss/pull/15067))
13+
1014
### Fixed
1115

16+
- Ensure `opacity` theme values configured as decimal numbers via JS config files work with color utilities ([#15067](https://github.com/tailwindlabs/tailwindcss/pull/15067))
1217
- _Upgrade (experimental)_: Include `color` in the form reset snippet ([#15064](https://github.com/tailwindlabs/tailwindcss/pull/15064))
1318

1419
## [4.0.0-alpha.36] - 2024-11-21

integrations/upgrade/index.test.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2939,3 +2939,137 @@ test(
29392939
)
29402940
},
29412941
)
2942+
2943+
test(
2944+
`upgrades opacity namespace values to percentages`,
2945+
{
2946+
fs: {
2947+
'package.json': json`
2948+
{
2949+
"dependencies": {
2950+
"tailwindcss": "^3",
2951+
"@tailwindcss/upgrade": "workspace:^"
2952+
},
2953+
"devDependencies": {
2954+
"@tailwindcss/cli": "workspace:^"
2955+
}
2956+
}
2957+
`,
2958+
'tailwind.config.js': js`
2959+
/** @type {import('tailwindcss').Config} */
2960+
module.exports = {
2961+
theme: {
2962+
opacity: {
2963+
0: '0',
2964+
2.5: '.025',
2965+
5: '.05',
2966+
7.5: 0.075,
2967+
10: 0.1,
2968+
2969+
semitransparent: '0.5',
2970+
transparent: 1,
2971+
2972+
50: '50%',
2973+
50.5: '50.5%',
2974+
'50.50': '50.5%',
2975+
'75%': '75%',
2976+
'100%': '100%',
2977+
},
2978+
},
2979+
content: ['./src/**/*.{html,js}'],
2980+
}
2981+
`,
2982+
'src/index.html': html`
2983+
<div
2984+
class="text-red-500/0
2985+
text-red-500/2.5
2986+
text-red-500/5
2987+
text-red-500/7.5
2988+
text-red-500/10
2989+
text-red-500/semitransparent
2990+
text-red-500/transparent
2991+
text-red-500/50
2992+
text-red-500/50.5
2993+
text-red-500/50.50
2994+
text-red-500/50%
2995+
text-red-500/100%"
2996+
></div>
2997+
`,
2998+
'src/input.css': css`
2999+
@tailwind base;
3000+
@tailwind components;
3001+
@tailwind utilities;
3002+
`,
3003+
},
3004+
},
3005+
async ({ exec, fs }) => {
3006+
await exec('npx @tailwindcss/upgrade')
3007+
3008+
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
3009+
"
3010+
--- ./src/index.html ---
3011+
<div
3012+
class="text-red-500/0
3013+
text-red-500/2.5
3014+
text-red-500/5
3015+
text-red-500/7.5
3016+
text-red-500/10
3017+
text-red-500/semitransparent
3018+
text-red-500/transparent
3019+
text-red-500/50
3020+
text-red-500/50.5
3021+
text-red-500/50.50
3022+
text-red-500/50%
3023+
text-red-500/100%"
3024+
></div>
3025+
3026+
--- ./src/input.css ---
3027+
@import 'tailwindcss';
3028+
3029+
@theme {
3030+
--opacity-*: initial;
3031+
--opacity-semitransparent: 50%;
3032+
--opacity-transparent: 100%;
3033+
--opacity-50_50: 50.5%;
3034+
--opacity-75\\%: 75%;
3035+
--opacity-100\\%: 100%;
3036+
}
3037+
3038+
/*
3039+
In Tailwind CSS v4, basic styles are applied to form elements by default. To
3040+
maintain compatibility with v3, the following resets have been added:
3041+
*/
3042+
@layer base {
3043+
input,
3044+
textarea,
3045+
select,
3046+
button {
3047+
border: 0px solid;
3048+
border-radius: 0;
3049+
padding: 0;
3050+
color: inherit;
3051+
background-color: transparent;
3052+
}
3053+
}
3054+
3055+
/*
3056+
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
3057+
so we've added these compatibility styles to make sure everything still
3058+
looks the same as it did with Tailwind CSS v3.
3059+
3060+
If we ever want to remove these styles, we need to add an explicit border
3061+
color utility to any element that depends on these defaults.
3062+
*/
3063+
@layer base {
3064+
*,
3065+
::after,
3066+
::before,
3067+
::backdrop,
3068+
::file-selector-button {
3069+
border-color: var(--color-gray-200, currentColor);
3070+
}
3071+
}
3072+
"
3073+
`)
3074+
},
3075+
)

packages/@tailwindcss-upgrade/src/migrate-js-config.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
1717
import type { Config } from '../../tailwindcss/src/compat/plugin-api'
1818
import type { DesignSystem } from '../../tailwindcss/src/design-system'
1919
import { escape } from '../../tailwindcss/src/utils/escape'
20-
import { isValidSpacingMultiplier } from '../../tailwindcss/src/utils/infer-data-type'
20+
import {
21+
isValidOpacityValue,
22+
isValidSpacingMultiplier,
23+
} from '../../tailwindcss/src/utils/infer-data-type'
2124
import { findStaticPlugins, type StaticPluginOptions } from './utils/extract-static-plugins'
2225
import { highlight, info, relative } from './utils/renderer'
2326

@@ -125,6 +128,25 @@ async function migrateTheme(
125128
value = value.replace(/\s*\/\s*<alpha-value>/, '').replace(/<alpha-value>/, '1')
126129
}
127130

131+
// Convert `opacity` namespace from decimal to percentage values.
132+
// Additionally we can drop values that resolve to the same value as the
133+
// named modifier with the same name.
134+
if (key[0] === 'opacity' && (typeof value === 'number' || typeof value === 'string')) {
135+
let numValue = typeof value === 'string' ? parseFloat(value) : value
136+
137+
if (numValue >= 0 && numValue <= 1) {
138+
value = numValue * 100 + '%'
139+
}
140+
141+
if (
142+
typeof value === 'string' &&
143+
key[1] === value.replace(/%$/, '') &&
144+
isValidOpacityValue(key[1])
145+
) {
146+
continue
147+
}
148+
}
149+
128150
if (key[0] === 'keyframes') {
129151
continue
130152
}

packages/tailwindcss/src/compat/apply-config-to-theme.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,34 @@ describe('keyPathToCssProperty', () => {
186186
expect(`--${keyPathToCssProperty(keyPath)}`).toEqual(expected)
187187
})
188188
})
189+
190+
test('converts opacity modifiers from decimal to percentage values', () => {
191+
let theme = new Theme()
192+
let design = buildDesignSystem(theme)
193+
194+
let { resolvedConfig, replacedThemeKeys } = resolveConfig(design, [
195+
{
196+
config: {
197+
theme: {
198+
opacity: {
199+
0: '0',
200+
5: '0.05',
201+
10: '0.1',
202+
15: '0.15',
203+
20: 0.2,
204+
25: 0.25,
205+
},
206+
},
207+
},
208+
base: '/root',
209+
},
210+
])
211+
applyConfigToTheme(design, resolvedConfig, replacedThemeKeys)
212+
213+
expect(theme.resolve('0', ['--opacity'])).toEqual('0%')
214+
expect(theme.resolve('5', ['--opacity'])).toEqual('5%')
215+
expect(theme.resolve('10', ['--opacity'])).toEqual('10%')
216+
expect(theme.resolve('15', ['--opacity'])).toEqual('15%')
217+
expect(theme.resolve('20', ['--opacity'])).toEqual('20%')
218+
expect(theme.resolve('25', ['--opacity'])).toEqual('25%')
219+
})

packages/tailwindcss/src/compat/apply-config-to-theme.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,20 @@ export function applyConfigToTheme(
3737
continue
3838
}
3939

40+
// Replace `<alpha-value>` with `1`
4041
if (typeof value === 'string') {
4142
value = value.replace(/<alpha-value>/g, '1')
4243
}
4344

45+
// Convert `opacity` namespace from decimal to percentage values
46+
if (path[0] === 'opacity' && (typeof value === 'number' || typeof value === 'string')) {
47+
let numValue = typeof value === 'string' ? parseFloat(value) : value
48+
49+
if (numValue >= 0 && numValue <= 1) {
50+
value = numValue * 100 + '%'
51+
}
52+
}
53+
4454
let name = keyPathToCssProperty(path)
4555
if (!name) continue
4656

0 commit comments

Comments
 (0)