Skip to content

Commit e578238

Browse files
Migrate supports theme keys (#18817)
This PR is a follow up of #18815 and #18816, but this time let's migrate the `supports` theme keys. Let's imagine you have the following Tailwind CSS v3 configuration: ```ts export default { content: ['./src/**/*.html'], theme: { extend: { supports: { // Automatically handled by bare values (using CSS variable as the value) foo: 'foo: var(--foo)', // parentheses are optional bar: '(bar: var(--bar))', // Not automatically handled because names differ baz: 'qux: var(--foo)', // ^^^ ^^^ ← different names // Custom grid: 'display: grid', }, }, }, } ``` Then we would generate the following Tailwind CSS v4 CSS: ```css @custom-variant supports-baz { @supports (qux: var(--foo)) { @slot; } } @custom-variant supports-grid { @supports (display: grid) { @slot; } } ``` Notice how we didn't generate a custom variant for `data-foo` or `data-bar` because those are automatically handled by bare values. I also went with the longer form of `@custom-variant`, we could use the single selector approach, but that felt less clear to me. ```css @custom-variant supports-baz (@supports (qux: var(--foo))); @custom-variant supports-grid (@supports (display: grid)); ``` --------- Co-authored-by: Jordan Pittman <[email protected]>
1 parent 82034ec commit e578238

File tree

3 files changed

+127
-7
lines changed

3 files changed

+127
-7
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Discard matched variants with unknown named values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799))
1818
- Discard matched variants with non-string values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799))
1919
- Show suggestions for known `matchVariant` values ([#18798](https://github.com/tailwindlabs/tailwindcss/pull/18798))
20-
- Migrate `aria` theme keys to `@custom-variant` ([#18815](https://github.com/tailwindlabs/tailwindcss/pull/18815))
21-
- Migrate `data` theme keys to `@custom-variant` ([#18816](https://github.com/tailwindlabs/tailwindcss/pull/18816))
20+
- Upgrade: Migrate `aria` theme keys to `@custom-variant` ([#18815](https://github.com/tailwindlabs/tailwindcss/pull/18815))
21+
- Upgrade: Migrate `data` theme keys to `@custom-variant` ([#18816](https://github.com/tailwindlabs/tailwindcss/pull/18816))
22+
- Upgrade: Migrate `supports` theme keys to `@custom-variant` ([#18817](https://github.com/tailwindlabs/tailwindcss/pull/18817))
2223

2324
## [4.1.12] - 2025-08-13
2425

integrations/upgrade/js-config.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,95 @@ test(
11221122
},
11231123
)
11241124

1125+
test(
1126+
'migrate supports theme keys to custom variants',
1127+
{
1128+
fs: {
1129+
'package.json': json`
1130+
{
1131+
"dependencies": {
1132+
"tailwindcss": "^3",
1133+
"@tailwindcss/upgrade": "workspace:^"
1134+
}
1135+
}
1136+
`,
1137+
'tailwind.config.ts': ts`
1138+
export default {
1139+
content: {
1140+
relative: true,
1141+
files: ['./src/**/*.html'],
1142+
},
1143+
theme: {
1144+
extend: {
1145+
supports: {
1146+
// Automatically handled by bare values (using CSS variable as the value)
1147+
foo: 'foo: var(--foo)', // parentheses are optional
1148+
bar: '(bar: var(--bar))',
1149+
1150+
// Not automatically handled by bare values because names differ
1151+
foo: 'bar: var(--foo)', // parentheses are optional
1152+
bar: '(qux: var(--bar))',
1153+
1154+
// Custom
1155+
grid: 'display: grid',
1156+
},
1157+
},
1158+
},
1159+
}
1160+
`,
1161+
'src/input.css': css`
1162+
@tailwind base;
1163+
@tailwind components;
1164+
@tailwind utilities;
1165+
`,
1166+
},
1167+
},
1168+
async ({ exec, fs, expect }) => {
1169+
await exec('npx @tailwindcss/upgrade')
1170+
1171+
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
1172+
"
1173+
--- src/input.css ---
1174+
@import 'tailwindcss';
1175+
1176+
@custom-variant supports-foo {
1177+
@supports (bar: var(--foo)) {
1178+
@slot;
1179+
}
1180+
}
1181+
@custom-variant supports-bar {
1182+
@supports ((qux: var(--bar))) {
1183+
@slot;
1184+
}
1185+
}
1186+
@custom-variant supports-grid {
1187+
@supports (display: grid) {
1188+
@slot;
1189+
}
1190+
}
1191+
1192+
/*
1193+
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
1194+
so we've added these compatibility styles to make sure everything still
1195+
looks the same as it did with Tailwind CSS v3.
1196+
1197+
If we ever want to remove these styles, we need to add an explicit border
1198+
color utility to any element that depends on these defaults.
1199+
*/
1200+
@layer base {
1201+
*,
1202+
::after,
1203+
::before,
1204+
::backdrop,
1205+
::file-selector-button {
1206+
border-color: var(--color-gray-200, currentcolor);
1207+
}
1208+
}
1209+
"
1210+
`)
1211+
},
1212+
)
1213+
11251214
describe('border compatibility', () => {
11261215
test(
11271216
'migrate border compatibility',

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

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
isValidOpacityValue,
2525
isValidSpacingMultiplier,
2626
} from '../../../../tailwindcss/src/utils/infer-data-type'
27+
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
2728
import { findStaticPlugins, type StaticPluginOptions } from '../../utils/extract-static-plugins'
2829
import { highlight, info, relative } from '../../utils/renderer'
2930

@@ -164,6 +165,35 @@ async function migrateTheme(
164165
}
165166
delete resolvedConfig.theme.data
166167
}
168+
169+
if ('supports' in resolvedConfig.theme) {
170+
for (let [key, value] of Object.entries(resolvedConfig.theme.supports ?? {})) {
171+
// Will be handled by bare values if the value of the declaration is a
172+
// CSS variable.
173+
let parsed = ValueParser.parse(`${value}`)
174+
175+
// Unwrap the parens, e.g.: `(foo: var(--bar))` → `foo: var(--bar)`
176+
if (parsed.length === 1 && parsed[0].kind === 'function' && parsed[0].value === '') {
177+
parsed = parsed[0].nodes
178+
}
179+
180+
// Verify structure: `foo: var(--bar)`
181+
// ^^^ ← must match the `key`
182+
if (
183+
parsed.length === 3 &&
184+
parsed[0].kind === 'word' &&
185+
parsed[0].value === key &&
186+
parsed[2].kind === 'function' &&
187+
parsed[2].value === 'var'
188+
) {
189+
continue
190+
}
191+
192+
// Create custom variant
193+
variants.set(`supports-${key}`, `{@supports(${value}){@slot;}}`)
194+
}
195+
delete resolvedConfig.theme.supports
196+
}
167197
}
168198

169199
// Convert theme values to CSS custom properties
@@ -242,7 +272,11 @@ async function migrateTheme(
242272
if (previousRoot !== root) css += '\n'
243273
previousRoot = root
244274

245-
css += `@custom-variant ${name} (${selector});\n`
275+
if (selector.startsWith('{')) {
276+
css += `@custom-variant ${name} ${selector}\n`
277+
} else {
278+
css += `@custom-variant ${name} (${selector});\n`
279+
}
246280
}
247281
css += '}\n'
248282
}
@@ -407,15 +441,11 @@ const ALLOWED_THEME_KEYS = [
407441
// Used by @tailwindcss/container-queries
408442
'containers',
409443
]
410-
const BLOCKED_THEME_KEYS = ['supports']
411444
function onlyAllowedThemeValues(theme: ThemeConfig): boolean {
412445
for (let key of Object.keys(theme)) {
413446
if (!ALLOWED_THEME_KEYS.includes(key)) {
414447
return false
415448
}
416-
if (BLOCKED_THEME_KEYS.includes(key)) {
417-
return false
418-
}
419449
}
420450

421451
if ('screens' in theme && typeof theme.screens === 'object' && theme.screens !== null) {

0 commit comments

Comments
 (0)