From 3aadba7cc117760182c862aced7c66b90e05a087 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 9 Oct 2025 19:30:01 +0200 Subject: [PATCH] Move modifier of `not-*`, `has-*`, and `in-*` variant to sub variant (#19100) This PR fixes an issue where a compound variant with a modifier such as `not-group-hover/name:flex` would not generate anything because the `/name` modifier belongs to the `not` variant, and not the compounded `group-hover` variant. This PR is a **workaround** (and definitely not perfect) by special casing the `not`, `has`, and `in` variants such that their modifiers are moved internally to the sub variant as-if the `/name` existed on `group-hover`. We don't do it for other compound variants such as `group` and `peer` because then `group-peer-focus/name:underline` would result in a breaking change: ```diff - .group-peer-focus\\/name\\:flex:is(:where(.group\\/name):is(:where(.peer):focus ~ *) *) + .group-peer-focus\/name\:flex:is(:where(.group):is(:where(.peer\/name):focus ~ *) *) ``` In case the diff is not clear, the name has moved: image This is also a limited workaround, because if you need multiple modifiers it won't work. I would've loved to special case this _inside_ the `not`, `has`, and `in` code that handles these variants, but we handle the variants in a depth-first way, so by the time you are handling the `not` variant, the sub variant was already handled... In a perfect world, you can use something like `not-group/name-hover` but then it becomes unambiguous because is `name` the name, is `name-hover`? ## Test plan Added a new test that wouldn't generate anything before this fix. Fixes: #15772 --- CHANGELOG.md | 3 ++- packages/tailwindcss/src/candidate.test.ts | 5 +++++ packages/tailwindcss/src/candidate.ts | 7 +++++++ packages/tailwindcss/src/variants.test.ts | 18 ++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a84ace364..52f712c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix Safari devtools rendering issue due to `color-mix` fallback ([#19069](https://github.com/tailwindlabs/tailwindcss/pull/19069)) -- Suppress Lightning CSS warnings about `:deep`, `:slotted` and `:global` ([#19094](https://github.com/tailwindlabs/tailwindcss/pull/19094)) +- Suppress Lightning CSS warnings about `:deep`, `:slotted`, and `:global` ([#19094](https://github.com/tailwindlabs/tailwindcss/pull/19094)) - Fix resolving theme keys when starting with the name of another theme key in JS configs and plugins ([#19097](https://github.com/tailwindlabs/tailwindcss/pull/19097)) +- Allow named groups in combination with `not-*`, `has-*`, and `in-*` ([#19100](https://github.com/tailwindlabs/tailwindcss/pull/19100)) ## [4.1.14] - 2025-10-01 diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index dfc65ac73..3d4aa983b 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -2102,6 +2102,11 @@ const variants = [ // Handle special `@` variants. These shouldn't be printed as `@-` ['@xl:', '@xl:'], ['@[123px]:', '@[123px]:'], + + // Compound variants that forward modifiers + ['not-group-hover/name:', 'not-group-hover/name:'], + ['has-group-peer-hover/name:', 'has-group-peer-hover/name:'], + ['in-group-peer-hover/name:', 'in-group-peer-hover/name:'], ] let combinations: [string, string][] = [] diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index a9ba2d6e2..df5998d9e 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -806,6 +806,13 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia case 'compound': { if (value === null) return null + // Forward the modifier of the compound variants to its subVariant. + // This allows for `not-group-hover/name:flex` to work. + if (modifier && (root === 'not' || root === 'has' || root === 'in')) { + value = `${value}/${modifier}` + modifier = null + } + let subVariant = designSystem.parseVariant(value) if (subVariant === null) return null diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index f9d054a21..a2540e698 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -2586,6 +2586,24 @@ test('matchVariant sorts deterministically', async () => { } }) +test('move modifier of compound variant to sub-variant if its also a compound variant', async () => { + expect( + await run([ + 'not-group-focus/name:flex', + 'has-group-focus/name:flex', + 'in-group-focus/name:flex', + + // Keep the `name` on the `group`, don't move it to the `peer` because + // that would be a breaking change. + 'group-peer-focus/name:flex', + ]), + ).toMatchInlineSnapshot(` + ".not-group-focus\\/name\\:flex:not(:is(:where(.group\\/name):focus *)), .group-peer-focus\\/name\\:flex:is(:where(.group\\/name):is(:where(.peer):focus ~ *) *), :where(:is(:where(.group\\/name):focus *)) .in-group-focus\\/name\\:flex, .has-group-focus\\/name\\:flex:has(:is(:where(.group\\/name):focus *)) { + display: flex; + }" + `) +}) + test.each([ // These are style rules [['.foo'], Compounds.StyleRules],