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 <thecrypticace@gmail.com>
This commit is contained in:
Robin Malfait 2025-08-28 16:50:48 +02:00 • committed by GitHub
parent 82034ec327
commit e578238da5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 127 additions and 7 deletions

View file

@ -17,8 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Discard matched variants with unknown named values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799))
- Discard matched variants with non-string values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799))
- Show suggestions for known `matchVariant` values ([#18798](https://github.com/tailwindlabs/tailwindcss/pull/18798))
- Migrate `aria` theme keys to `@custom-variant` ([#18815](https://github.com/tailwindlabs/tailwindcss/pull/18815))
- Migrate `data` theme keys to `@custom-variant` ([#18816](https://github.com/tailwindlabs/tailwindcss/pull/18816))
- Upgrade: Migrate `aria` theme keys to `@custom-variant` ([#18815](https://github.com/tailwindlabs/tailwindcss/pull/18815))
- Upgrade: Migrate `data` theme keys to `@custom-variant` ([#18816](https://github.com/tailwindlabs/tailwindcss/pull/18816))
- Upgrade: Migrate `supports` theme keys to `@custom-variant` ([#18817](https://github.com/tailwindlabs/tailwindcss/pull/18817))
## [4.1.12] - 2025-08-13

View file

@ -1122,6 +1122,95 @@ test(
},
)
test(
'migrate supports theme keys to custom variants',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
export default {
content: {
relative: true,
files: ['./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 by bare values because names differ
foo: 'bar: var(--foo)', // parentheses are optional
bar: '(qux: var(--bar))',
// Custom
grid: 'display: grid',
},
},
},
}
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@custom-variant supports-foo {
@supports (bar: var(--foo)) {
@slot;
}
}
@custom-variant supports-bar {
@supports ((qux: var(--bar))) {
@slot;
}
}
@custom-variant supports-grid {
@supports (display: grid) {
@slot;
}
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
},
)
describe('border compatibility', () => {
test(
'migrate border compatibility',

View file

@ -24,6 +24,7 @@ import {
isValidOpacityValue,
isValidSpacingMultiplier,
} from '../../../../tailwindcss/src/utils/infer-data-type'
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
import { findStaticPlugins, type StaticPluginOptions } from '../../utils/extract-static-plugins'
import { highlight, info, relative } from '../../utils/renderer'
@ -164,6 +165,35 @@ async function migrateTheme(
}
delete resolvedConfig.theme.data
}
if ('supports' in resolvedConfig.theme) {
for (let [key, value] of Object.entries(resolvedConfig.theme.supports ?? {})) {
// Will be handled by bare values if the value of the declaration is a
// CSS variable.
let parsed = ValueParser.parse(`${value}`)
// Unwrap the parens, e.g.: `(foo: var(--bar))` → `foo: var(--bar)`
if (parsed.length === 1 && parsed[0].kind === 'function' && parsed[0].value === '') {
parsed = parsed[0].nodes
}
// Verify structure: `foo: var(--bar)`
// ^^^ ← must match the `key`
if (
parsed.length === 3 &&
parsed[0].kind === 'word' &&
parsed[0].value === key &&
parsed[2].kind === 'function' &&
parsed[2].value === 'var'
) {
continue
}
// Create custom variant
variants.set(`supports-${key}`, `{@supports(${value}){@slot;}}`)
}
delete resolvedConfig.theme.supports
}
}
// Convert theme values to CSS custom properties
@ -242,7 +272,11 @@ async function migrateTheme(
if (previousRoot !== root) css += '\n'
previousRoot = root
css += `@custom-variant ${name} (${selector});\n`
if (selector.startsWith('{')) {
css += `@custom-variant ${name} ${selector}\n`
} else {
css += `@custom-variant ${name} (${selector});\n`
}
}
css += '}\n'
}
@ -407,15 +441,11 @@ const ALLOWED_THEME_KEYS = [
// Used by @tailwindcss/container-queries
'containers',
]
const BLOCKED_THEME_KEYS = ['supports']
function onlyAllowedThemeValues(theme: ThemeConfig): boolean {
for (let key of Object.keys(theme)) {
if (!ALLOWED_THEME_KEYS.includes(key)) {
return false
}
if (BLOCKED_THEME_KEYS.includes(key)) {
return false
}
}
if ('screens' in theme && typeof theme.screens === 'object' && theme.screens !== null) {