diff --git a/CHANGELOG.md b/CHANGELOG.md index 06e053c3e..bf22c654d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Canonicalization: preserve significant `_` whitespace in arbitrary values ([#19986](https://github.com/tailwindlabs/tailwindcss/pull/19986)) - Canonicalization: add parentheses when removing whitespace from arbitrary values would hurt readability ([#19986](https://github.com/tailwindlabs/tailwindcss/pull/19986)) - Canonicalization: preserve the original unit in arbitrary values instead of normalizing to base units (e.g. `-mt-[20in]` → `mt-[-20in]`, not `mt-[-1920px]`) ([#19988](https://github.com/tailwindlabs/tailwindcss/pull/19988)) +- Canonicalization: migrate arbitrary `:has()` variants from `[&:has(…)]` to `has-[…]` ([#19991](https://github.com/tailwindlabs/tailwindcss/pull/19991)) ## [4.2.4] - 2026-04-21 diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 5e9bc166c..184414da3 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -1022,6 +1022,11 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['has-[[aria-visible]]:flex', 'has-aria-[visible]:flex'], ['has-[&:not(:nth-child(even))]:flex', 'has-odd:flex'], + + // Arbitrary variant to compound + arbitrary variants + ['[&:has([role=checkbox])]:flex', 'has-[[role=checkbox]]:flex'], + ['[&:has([aria-visible="true"])]:flex', 'has-aria-visible:flex'], + ['[&:has([data-slot=description])]:flex', 'has-data-[slot=description]:flex'], ])(testName, { timeout }, async (candidate, expected) => { let input = css` @import 'tailwindcss'; diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 7cfdbf84c..118f51e60 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -1796,6 +1796,28 @@ function modernizeArbitraryValuesVariant( continue } + // `[&:has(…)]` can be replaced with `has-[…]` + if ( + // Only top-level, so `group-[&:has(…)]` is not covered + + parent === null && + // [&:has(…)]:flex + // ^ ^^^^^^ + ast.length === 2 && + ast[0].kind === 'selector' && + ast[0].value === '&' && + ast[1].kind === 'function' && + ast[1].value === ':has' && + ast[1].nodes.length === 1 && + ast[1].nodes[0].kind === 'selector' + ) { + replaceObject( + variant, + designSystem.parseVariant(`has-[${SelectorParser.toCss(ast[1].nodes)}]`), + ) + continue + } + // `in-*` variant. If the selector ends with ` &`, we can convert it to an // `in-*` variant. //