Skip to content

Simplify @variant usage, allow compound and stacked variants#19996

Merged
RobinMalfait merged 11 commits into
mainfrom
feat/simplify-at-variant-usage
Apr 30, 2026
Merged

Simplify @variant usage, allow compound and stacked variants#19996
RobinMalfait merged 11 commits into
mainfrom
feat/simplify-at-variant-usage

Conversation

@RobinMalfait
Copy link
Copy Markdown
Member

@RobinMalfait RobinMalfait commented Apr 30, 2026

This PR improves and simplifies the @variant usage.

When we originally added support for @variant, we wanted to keep things simple, where we could only use a single variant at a time. The original PR did have a more complex system with all these features enabled, but we wanted to make sure that we only introduced the additional complexity when the community felt like it was needed.

But of course we still wanted to make sure that you could do compound and stacked variants, it just required some additional code.

For compound variants, where you want to use variant a and variant b, you could duplicate the rules as siblings:

.foo {
  @variant a {
    display: flex;
  }

  @variant b {
    display: flex;
  }
}

But with this PR, you can comma separate each variant to get the same effect:

.foo {
  @variant a, b {
    display: flex;
  }
}

You can think of this as-if we are expanding this syntax into the aforementioned syntax. In other words, we would do the duplication for you.

Additionally, you also want to be able to stack variants. For that you had to nest your @variant rules:

.foo {
  @variant a {
    @variant b {
      display: flex;
    }
  }
}

Not the end of the world, but it can get pretty nested if you want to use multiple variants. Luckily we already have a syntax for this in normal Tailwind CSS classes: a:b:flex. Which is exactly what we can use here as well:

.foo {
  @variant a:b {
    display: flex;
  }
}

Again, conceptually you can think of this syntax being expanded into the syntax from above.

Last but not least, we can also combine these:

.foo {
  background: black;

  @variant a, b:c {
    background: red;

    @variant d, e:f {
      background: blue;
    }
  }
}

This conceptually translates into the much more verbose version today:

.foo {
  background: black;

  @variant a {
    background: red;

    @variant d {
      background: blue;
    }

    @variant e {
      @variant f {
        background: blue;
      }
    }
  }

  @variant b {
    @variant c {
      background: red;

      @variant d {
        background: blue;
      }

      @variant e {
        @variant f {
          background: blue;
        }
      }
    }
  }
}

The biggest downside is that this could potentially easily balloon your CSS file size if you're not careful. Because with this, it's pretty easy to add one more variant that introduces a lot of duplicated CSS.

This feature is completely backwards compatible, you can still nest your @variant calls yourself if you want, and combine them with these features if you want.

This is also a continuation of #19526 and #19884, but for some reason I don't have push rights, so I'm creating this new PR instead. I did keep the original commits of those PRs so these contributors are still properly marked as contributors.
image
image

Closes: #19526
Closes: #19884

Test plan

  1. Added a bunch of new tests to verify this new behavior
  2. Added tests that compare the short (new) version, and the long (old) version
  3. Added a sourcemap related test to ensure that the src and dst locations are correct
  4. Existing tests still pass

@RobinMalfait RobinMalfait requested a review from a team as a code owner April 30, 2026 09:55
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b03f3d24-ea79-4e72-849f-136d022e52e8

📥 Commits

Reviewing files that changed from the base of the PR and between 65be6bf and c03ac88.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • packages/tailwindcss/src/index.test.ts
  • packages/tailwindcss/src/source-maps/source-map.test.ts
  • packages/tailwindcss/src/variants.ts
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/tailwindcss/src/index.test.ts

Walkthrough

The change updates @variant parsing and expansion to handle comma-separated compound variants and stacked colon-separated variants. substituteAtVariant iterates each compound segment, clones AST nodes as needed to preserve source-map metadata, and applies stacked variants right-to-left, throwing on empty tokens or failed applications. It replaces a single @variant at-rule with multiple generated rules. New Vitest cases cover comma/colon edge cases, malformed inputs (trailing/double commas), mixed formats, stacked/compound interactions, and source-map mappings for the expanded variants.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Simplify @variant usage, allow compound and stacked variants' is directly related to the main change, clearly summarizing the new compound and stacked variant support.
Description check ✅ Passed The description thoroughly explains the new @variant syntax features, provides clear examples of compound and stacked variants, and documents the backwards compatibility and test plan.
Linked Issues check ✅ Passed The PR fully implements comma-separated compound variants [#19526] and colon-delimited stacked variants [#19884], with comprehensive tests comparing short and long forms, plus a sourcemap test.
Out of Scope Changes check ✅ Passed All changes are scoped to @variant functionality: tests for new behaviors, implementation in variants.ts, and changelog entries. No unrelated modifications detected.

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


Review rate limit: 3/5 reviews remaining, refill in 18 minutes and 10 seconds.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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)

5519-5549: ⚡ Quick win

Make whitespace coverage deterministic instead of random sampling.

Line 5520 and Line 5521 use random generation, which makes this regression test less reproducible and can miss edge combinations. Prefer an explicit matrix (test.each) for stable coverage.

♻️ Suggested deterministic rewrite
-    it('should handle optional whitespace between `@variant` variants', async () => {
-      let before = ['', ' ', '\t'][(Math.random() * 3) | 0].repeat((Math.random() * 3) | 0)
-      let after = ['', ' ', '\t'][(Math.random() * 3) | 0].repeat((Math.random() * 3) | 0)
-
-      await expect(
-        compileCss(css`
-          .btn {
-            background: black;
-
-            `@variant` hover${before},${after}focus {
-              background: red;
-            }
-          }
-          `@tailwind` utilities;
-        `),
-      ).resolves.toMatchInlineSnapshot(`
+    it.each([
+      ['', ''],
+      [' ', ''],
+      ['', ' '],
+      ['\t', ''],
+      ['', '\t'],
+      ['  ', '\t'],
+      ['\t\t', '  '],
+    ])(
+      'should handle optional whitespace between `@variant` variants (%j,%j)',
+      async (before, after) => {
+        await expect(
+          compileCss(css`
+            .btn {
+              background: black;
+
+              `@variant` hover${before},${after}focus {
+                background: red;
+              }
+            }
+            `@tailwind` utilities;
+          `),
+        ).resolves.toMatchInlineSnapshot(`
         ".btn {
           background: `#000`;
         }

         `@media` (hover: hover) {
           .btn:hover {
             background: red;
           }
         }

         .btn:focus {
           background: red;
         }"
-      `)
-    })
+        `)
+      },
+    )
🤖 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 5519 - 5549, The test
"should handle optional whitespace between `@variant` variants" currently uses
Math.random to build `before` and `after` whitespace which makes the test
nondeterministic; replace the randomized whitespace generation (the Math.random
branches used to build `before` and `after`) with an explicit deterministic
matrix using test.each (or describe.each) that iterates over all needed
whitespace combos (e.g., '', ' ', '\t', multiple repeats) and runs the same
assertion body calling compileCss, keeping the test title and using the same
compileCss/expect/resolves.toMatchInlineSnapshot assertions so behavior is
unchanged but coverage is deterministic.
🤖 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 5519-5549: The test "should handle optional whitespace between
`@variant` variants" currently uses Math.random to build `before` and `after`
whitespace which makes the test nondeterministic; replace the randomized
whitespace generation (the Math.random branches used to build `before` and
`after`) with an explicit deterministic matrix using test.each (or
describe.each) that iterates over all needed whitespace combos (e.g., '', ' ',
'\t', multiple repeats) and runs the same assertion body calling compileCss,
keeping the test title and using the same
compileCss/expect/resolves.toMatchInlineSnapshot assertions so behavior is
unchanged but coverage is deterministic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8eb24179-c115-4122-a595-9b50d2c2d417

📥 Commits

Reviewing files that changed from the base of the PR and between 6cf1af2 and 7462d0a.

📒 Files selected for processing (3)
  • packages/tailwindcss/src/index.test.ts
  • packages/tailwindcss/src/source-maps/source-map.test.ts
  • packages/tailwindcss/src/variants.ts

Instead of creating a new array for every map and reverse action, we
only create the necessary ones as we go. Then we loop through the array
in reverse instead of reversing and looping normally.
@RobinMalfait RobinMalfait force-pushed the feat/simplify-at-variant-usage branch from 65be6bf to c03ac88 Compare April 30, 2026 10:13
@RobinMalfait RobinMalfait merged commit e1201bc into main Apr 30, 2026
9 checks passed
@RobinMalfait RobinMalfait deleted the feat/simplify-at-variant-usage branch April 30, 2026 10:19
RobinMalfait added a commit that referenced this pull request Apr 30, 2026
This PR is an internal change only related to how we visualize source
maps.

As part of this PR
(#19996) I added a test
for source maps related to how `@variant` is processed. And while the
result was correct, I had a hard time verifying if this was _actually_
correct. I did the mental mapping of comparing locations from the output
to the input.
<img width="495" height="495" alt="image"
src="https://github.com/user-attachments/assets/2b1c12cd-43ee-461a-b93b-bafe6ce1cce5"
/>

With this PR, I want to make that more visual by actually printing the
input source(s) and output file and highlight the necessary parts:
<img width="1101" height="1085" alt="image"
src="https://github.com/user-attachments/assets/14390026-f211-4cfc-8c6c-3293105f6403"
/>

I didn't want to get too clever here. But printing line numbers also
helps in case we point to different spots on the same line:
<img width="1200" height="981" alt="image"
src="https://github.com/user-attachments/assets/e75f7622-eb51-49ee-928e-fdc8672d2c3f"
/>

And if we point to different files, then we visualize these as well:
<img width="851" height="957" alt="image"
src="https://github.com/user-attachments/assets/77e65801-b46b-4579-8659-d919a8750f60"
/>


If you combine this with snapshot tests, then it's very easy to verify
that locations match up correctly. Each source map location is
highlighted and references a symbol starting at `A`, `B`, etc.

A change to the source maps _can_ result in a big diff, and even this PR
introduces a big diff because of the preflight diffs. But at least you
can see how things line up.

## Test plan

1. All tests still pass
2. Added tests for the source map visualizer that is only used in tests
3. No actual source code was touched
@ben-rogerson
Copy link
Copy Markdown

👏

RobinMalfait added a commit to tailwindlabs/tailwindcss.com that referenced this pull request May 11, 2026
…ants (#2481)

Direct link:
https://tailwindcss-com-git-feat-document-compound-9d8eca-tailwindlabs.vercel.app/docs/adding-custom-styles#using-variants

This PR documents the new `@variant` behavior that was introduced in
tailwindlabs/tailwindcss#19996 where you can
stack variants (like you would in normal HTML):

```css
.my-element {
  background: white;
  @variant hover:focus {
    background: black;
  }
}
```

Which compiles to:
```css
.my-element {
  background: white;

  &:hover {
    @media (hover: hover) {
      &:focus {
        background: black;
      }
    }
  }
}
```

Similarly, this adds docs for compound variants, where you can define
the same CSS in different situations:

```css
.my-element {
  background: white;
  @variant hover, focus {
    background: black;
  }
}
```

Which compiles to:
```css
.my-element {
  background: white;
  &:hover {
    @media (hover: hover) {
      background: black;
    }
  }

  &:focus {
    background: black;
  }
}
```

<img width="709" height="1251" alt="image"
src="https://github.com/user-attachments/assets/46089844-06c9-4640-ab3b-a05d69bea8f9"
/>
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.

4 participants