diff --git a/CHANGELOG.md b/CHANGELOG.md index 3801ae9d3972..cda136124fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 808b62013f6b..de10250c29a1 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -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', diff --git a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts index c7fb046f4798..24afe56b85b5 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts @@ -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) {