fix(react-email): extract media-query variants with tailwindcss 4.3.3+#3674
fix(react-email): extract media-query variants with tailwindcss 4.3.3+#3674dielduarte wants to merge 7 commits into
Conversation
Tailwind 4.3.3 inverted the CSS it generates for variant utilities like `dark:`: instead of nesting the `@media` inside the class rule, it now wraps the class rule with the `@media`. The extractor walked each rule in isolation, so the wrapped rule looked like a plain inlinable class — its conditional value was inlined as the base style and the `@media` rule was never emitted into the <head>. Track the enclosing @media/@supports at-rules while walking and normalize the wrapped shape back to the nested shape the rest of the pipeline expects, so extraction behaves identically across Tailwind versions.
🦋 Changeset detectedLatest commit: 8475301 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
commit: |
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Auto-approved: Fix for Tailwind 4.3.3+ CSS shape change in variant extraction; adds normalization and tests.
Re-trigger cubic
There was a problem hiding this comment.
1 issue found and verified against the latest diff
Confidence score: 3/5
- In
packages/react-email/src/components/tailwind/utils/css/extract-rules-per-class.ts, stacked conditional variants are still emitted with nested@media/@supportsinside a selector instead of fully downleveled CSS, so some email clients that don’t support nesting may drop those styles entirely and render incorrect layouts — flatten nested at-rules during extraction (or add a post-process pass/test for stacked variants) before merging.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/react-email/src/components/tailwind/utils/css/extract-rules-per-class.ts">
<violation number="1" location="packages/react-email/src/components/tailwind/utils/css/extract-rules-per-class.ts:115">
P2: Stacked conditional variants still emit CSS nesting instead of fully downleveled email CSS. After the outer wrapper is moved, the inner `@media`/`@supports` remains inside the selector, so clients without CSS nesting support can drop combinations such as responsive dark/supports utilities; make unnesting recursive or repeat it until no nested conditional at-rules remain.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // declarations only apply in that condition, so treat the whole thing as | ||
| // non-inlinable after normalizing it to the nested-at-rule shape. | ||
| if (enclosingAtRules.length > 0) { | ||
| const nonInlinablePart = nestAtRulesInsideRule(rule, enclosingAtRules); |
There was a problem hiding this comment.
P2: Stacked conditional variants still emit CSS nesting instead of fully downleveled email CSS. After the outer wrapper is moved, the inner @media/@supports remains inside the selector, so clients without CSS nesting support can drop combinations such as responsive dark/supports utilities; make unnesting recursive or repeat it until no nested conditional at-rules remain.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-email/src/components/tailwind/utils/css/extract-rules-per-class.ts, line 115:
<comment>Stacked conditional variants still emit CSS nesting instead of fully downleveled email CSS. After the outer wrapper is moved, the inner `@media`/`@supports` remains inside the selector, so clients without CSS nesting support can drop combinations such as responsive dark/supports utilities; make unnesting recursive or repeat it until no nested conditional at-rules remain.</comment>
<file context>
@@ -23,52 +77,90 @@ export function extractRulesPerClass(root: CssNode, classes: string[]) {
+ // declarations only apply in that condition, so treat the whole thing as
+ // non-inlinable after normalizing it to the nested-at-rule shape.
+ if (enclosingAtRules.length > 0) {
+ const nonInlinablePart = nestAtRulesInsideRule(rule, enclosingAtRules);
+ for (const className of selectorClasses) {
+ if (classSet.has(className)) {
</file context>
There was a problem hiding this comment.
unsure if we should fix this in this PR @gabrielmfern it was already an existing behavior, I'm leaning towards not doing it here. thoughts?
There was a problem hiding this comment.
yeah it looks like a separate concern, also not sure it makes sense either
if I understand correctly, cubic is just saying that we don't inline non inlinable styles? I mean, there's no way to inline them haha
Dismissed because Cubic found issues in a newer review.
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
Replace the sync-by-comment with a single source of truth in constants.ts, imported by both extract-rules-per-class.ts and downlevel-for-email-clients.ts, so the two passes can't drift out of sync.
There was a problem hiding this comment.
0 issues found across 3 files (changes from recent commits).
Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Auto-approved: Fixes a bounded bug where Tailwind 4.3.3+ drops media-query variants by normalizing the new wrapped @media shape to the old nested shape; covered by new tests.
Re-trigger cubic
| `); | ||
| }); | ||
|
|
||
| // Tailwind >=4.3.3 changed the shape of variant utilities: instead of nesting |
There was a problem hiding this comment.
how about we just mention the issue's url here instead? like a
// see https://...
There was a problem hiding this comment.
agree! I am hating all these comments, but trying to keep ones explaining why code exists the code itself it not enough
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Auto-approved: Fixes a Tailwind version compatibility bug by re-nesting wrapped @media rules, restoring correct dark mode extraction. Bounded, clearly beneficial fix with tests.
Re-trigger cubic
Problem
With
tailwindcss@4.3.3+,<Tailwind>stops extracting media-query variants (e.g.dark:) into the<head><style>block. Instead, the conditional value is inlined as the base style and the@mediarule is dropped entirely — sodark:colors apply unconditionally and dark-mode switching never happens.Root cause
Tailwind changed the shape of the CSS it generates for variant utilities between
4.3.2and4.3.3.≤4.3.2 nests the
@mediainside the class rule:≥4.3.3 inverts it — the
@mediawraps the class rule:extractRulesPerClasswalks eachRulenode in isolation. With the new shape, the inner.dark\:bg-blackrule looks like a plain class with a single declaration — no@media, no pseudo — soisRuleInlinable()returnstrue, the value gets inlined, and the@mediawrapper is never emitted.Fix
Track the chain of enclosing
@media/@supportsat-rules while walking. When a rule is found inside one, treat it as non-inlinable and normalize it back to the older nested-at-rule shape, so the rest of the pipeline (downlevelForEmailClients, etc.) behaves identically across Tailwind versions. This keeps compatibility going forward rather than capping the supported Tailwind range.Tests
Added two companion tests to
extract-rules-per-class.spec.tsthat assert both CSS shapes produce identical classification:treats @media-nested rules (Tailwind <=4.3.2) as non-inlinabletreats @media-wrapped rules (Tailwind >=4.3.3) as non-inlinableBoth are parsed directly (version-independent) since the repo is pinned to
tailwindcss@4.1.18, which only emits the old shape. Full tailwind suite passes (139), typecheck and lint clean.Summary by cubic
Fixes dropping
dark:and other media-query variants withtailwindcss@4.3.3+. Conditional styles are now extracted into the again so dark mode and other media-based variants work as expected.Bug Fixes
@media/@supportsat-rules while walking CSS and treat inner class rules as non-inlinable.4.3.3) and nested (≤4.3.2) shapes produce identical extraction.Refactors
NON_INLINABLE_ATRULESinconstants.tsand shared it between the extractor and downlevel passes to keep behavior aligned.#3662for context.Written for commit 8475301. Summary will update on new commits.