Skip to content

feat: allow using @variant with stacked variants#19884

Closed
ArrayKnight wants to merge 5 commits into
tailwindlabs:mainfrom
ArrayKnight:feat/support-compound-variants-in-css
Closed

feat: allow using @variant with stacked variants#19884
ArrayKnight wants to merge 5 commits into
tailwindlabs:mainfrom
ArrayKnight:feat/support-compound-variants-in-css

Conversation

@ArrayKnight

@ArrayKnight ArrayKnight commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Extends: #19526
Rationale: #19883

Summary

in css, you can now apply the same styles to stacked variants (similar to the inline syntax for class names) in one place. e.g.

@variant hover:focus {
    background-color: red;
}

Test plan

  • all existing unit tests should pass
  • created suggested missing tests for @variant with comma-separated values
  • created new unit tests to test @variant with stacked variants

@ArrayKnight ArrayKnight requested a review from a team as a code owner March 31, 2026 21:33
@ArrayKnight ArrayKnight changed the title Feat/support compound variants in css feat: allow using @variant with compound variants Mar 31, 2026
@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

The PR adds tests for comma-separated, whitespace-tolerant, nested, and stacked @variant usages and for error cases with empty variants. It updates substituteAtVariant to split variantNode.params by commas into groups, split each group by : into trimmed, reversed segments, clone the & rule per group, parse and apply each segment via parseVariant/applyVariant, throw on empty/unknown/failed segments, and replace the original @variant at-rule with the generated nodes.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature addition: allowing @variant to work with stacked variants like 'hover:focus'.
Description check ✅ Passed The description is directly related to the changeset, explaining the feature, providing clear examples, and documenting the test plan.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/tailwindcss/src/variants.ts (1)

1215-1232: Reuse the candidate-side variant tokenizer here.

This is now a second parser for stacked-variant syntax. If packages/tailwindcss/src/candidate.ts picks up new escaping/bracketing rules, @variant can drift from utility-candidate parsing. I'd rather extract the comma -> group -> colon tokenization into a shared helper than open-code it in both places.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tailwindcss/src/variants.ts` around lines 1215 - 1232, The code
duplicates candidate-side stacked-variant tokenization (segment(..., ',') then
segment(..., ':')), which risks drift; extract that logic into a shared helper
(e.g., export a parseStackedVariants or tokenizeVariantSequences function) and
use it from both packages/tailwindcss/src/variants.ts and
packages/tailwindcss/src/candidate.ts; update variants.ts to replace the inline
segment(...) chain that builds selectors with a call to the new helper (working
on variantNode.params) and keep the subsequent loop using selectors, so callers
like the loop that creates node = styleRule('&',
variantNode.nodes.map(cloneAstNode)) and designSystem.parseVariant(variant)
continue to work unchanged.
packages/tailwindcss/src/index.test.ts (1)

5551-5611: Add one true compound-variant case here.

This block currently exercises stacked static variants (hover:focus), but not a kind: 'compound' variant like group-hover, peer-focus, or not-hover that still goes through designSystem.parseVariant(...). One such case would better lock in the behavior this PR is aiming at.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tailwindcss/src/index.test.ts` around lines 5551 - 5611, Add a true
“compound” variant test inside the describe('compound `@variant` rules') block
to exercise designSystem.parseVariant with kind: 'compound' (e.g., use a variant
like group-hover or peer-focus), not just stacked static variants; copy the
existing test structure (the compileCss call and snapshot assertion) and add a
new it(...) that uses `@variant` group-hover:focus (or `@variant` peer-focus:active)
in the CSS and assert the expected generated selector (e.g., .group:hover
.btn:focus { ... } for group-hover) so the snapshot verifies the compound
variant path is exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/tailwindcss/src/index.test.ts`:
- Around line 5551-5611: Add a true “compound” variant test inside the
describe('compound `@variant` rules') block to exercise
designSystem.parseVariant with kind: 'compound' (e.g., use a variant like
group-hover or peer-focus), not just stacked static variants; copy the existing
test structure (the compileCss call and snapshot assertion) and add a new
it(...) that uses `@variant` group-hover:focus (or `@variant` peer-focus:active) in
the CSS and assert the expected generated selector (e.g., .group:hover
.btn:focus { ... } for group-hover) so the snapshot verifies the compound
variant path is exercised.

In `@packages/tailwindcss/src/variants.ts`:
- Around line 1215-1232: The code duplicates candidate-side stacked-variant
tokenization (segment(..., ',') then segment(..., ':')), which risks drift;
extract that logic into a shared helper (e.g., export a parseStackedVariants or
tokenizeVariantSequences function) and use it from both
packages/tailwindcss/src/variants.ts and packages/tailwindcss/src/candidate.ts;
update variants.ts to replace the inline segment(...) chain that builds
selectors with a call to the new helper (working on variantNode.params) and keep
the subsequent loop using selectors, so callers like the loop that creates node
= styleRule('&', variantNode.nodes.map(cloneAstNode)) and
designSystem.parseVariant(variant) continue to work unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d480b769-4707-4809-b2fc-ea02689f81dc

📥 Commits

Reviewing files that changed from the base of the PR and between d7fc281 and f93c53a.

📒 Files selected for processing (2)
  • packages/tailwindcss/src/index.test.ts
  • packages/tailwindcss/src/variants.ts

@ArrayKnight ArrayKnight changed the title feat: allow using @variant with compound variants feat: allow using @variant with stacked variants Apr 1, 2026
@RobinMalfait RobinMalfait force-pushed the feat/support-compound-variants-in-css branch from 5be2bb6 to 8a6d6d4 Compare April 29, 2026 21:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/tailwindcss/src/index.test.ts (1)

5462-5736: Add one regression for arbitrary variants containing internal commas/colons.

The new parsing path now relies on segment() not splitting inside bracketed arbitrary variants. A case like @variant [&:is(:hover,:focus)], disabled { ... } would lock that behavior in and help prevent future parser regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tailwindcss/src/index.test.ts` around lines 5462 - 5736, Add a
regression test in the existing "comma-separated `@variant` rules" suite that
verifies arbitrary variants containing internal commas/colons are not split (use
compileCss and css helpers): create an it(...) that uses `@variant`
[&:is(:hover,:focus)], disabled { ... } inside a .btn rule and assert the
compiled output includes the combined selector for the arbitrary variant (e.g.
.btn[&:is(:hover,:focus)] -> translated correctly) and the separate
.btn:disabled rule; reference compileCss and css to locate where to add the
test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/tailwindcss/src/index.test.ts`:
- Around line 5462-5736: Add a regression test in the existing "comma-separated
`@variant` rules" suite that verifies arbitrary variants containing internal
commas/colons are not split (use compileCss and css helpers): create an it(...)
that uses `@variant` [&:is(:hover,:focus)], disabled { ... } inside a .btn rule
and assert the compiled output includes the combined selector for the arbitrary
variant (e.g. .btn[&:is(:hover,:focus)] -> translated correctly) and the
separate .btn:disabled rule; reference compileCss and css to locate where to add
the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 538bbc7a-4c3c-43e2-8a08-f98af87c1420

📥 Commits

Reviewing files that changed from the base of the PR and between 5be2bb6 and 8a6d6d4.

📒 Files selected for processing (2)
  • packages/tailwindcss/src/index.test.ts
  • packages/tailwindcss/src/variants.ts

@RobinMalfait

Copy link
Copy Markdown
Member

Hey! Thanks for the PR, I implemented this as part of #19996 but your commits are still included so you are still marked as a contributor.

I made some additional changes, added some test etc. But somehow, I didn't have push access, despite GitHub showing I have push access:
image
image
Which is why I created the new PR, thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants