Skip to content

fix(react-email): extract media-query variants with tailwindcss 4.3.3+#3674

Open
dielduarte wants to merge 7 commits into
canaryfrom
fix/tailwind-4.3.3-media-variant-extraction
Open

fix(react-email): extract media-query variants with tailwindcss 4.3.3+#3674
dielduarte wants to merge 7 commits into
canaryfrom
fix/tailwind-4.3.3-media-variant-extraction

Conversation

@dielduarte

@dielduarte dielduarte commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

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 @media rule is dropped entirely — so dark: 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.2 and 4.3.3.

≤4.3.2 nests the @media inside the class rule:

.dark\:bg-black { @media (prefers-color-scheme: dark) { background-color: #000 } }

≥4.3.3 inverts it — the @media wraps the class rule:

@media (prefers-color-scheme: dark) { .dark\:bg-black { background-color: #000 } }

extractRulesPerClass walks each Rule node in isolation. With the new shape, the inner .dark\:bg-black rule looks like a plain class with a single declaration — no @media, no pseudo — so isRuleInlinable() returns true, the value gets inlined, and the @media wrapper is never emitted.

Fix

Track the chain of enclosing @media/@supports at-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.ts that assert both CSS shapes produce identical classification:

  • treats @media-nested rules (Tailwind <=4.3.2) as non-inlinable
  • treats @media-wrapped rules (Tailwind >=4.3.3) as non-inlinable

Both 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 with tailwindcss@4.3.3+. Conditional styles are now extracted into the again so dark mode and other media-based variants work as expected.

  • Bug Fixes

    • Track enclosing @media/@supports at-rules while walking CSS and treat inner class rules as non-inlinable.
    • Re-nest wrapped rules back to the older nested shape for consistent downstream processing.
    • Added tests to ensure both wrapped (≥4.3.3) and nested (≤4.3.2) shapes produce identical extraction.
  • Refactors

    • Centralized NON_INLINABLE_ATRULES in constants.ts and shared it between the extractor and downlevel passes to keep behavior aligned.
    • Comments reference issue #3662 for context.

Written for commit 8475301. Summary will update on new commits.

Review in cubic

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-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8475301

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
react-email Patch
@react-email/editor Patch
@react-email/ui Patch

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

@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-email Ready Ready Preview, Comment Jul 21, 2026 5:54pm
react-email-demo Ready Ready Preview, Comment Jul 21, 2026 5:54pm

@pkg-pr-new

pkg-pr-new Bot commented Jul 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/react-email@3674

commit: 8475301

cubic-dev-ai[bot]
cubic-dev-ai Bot previously approved these changes Jul 21, 2026

@cubic-dev-ai cubic-dev-ai 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.

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

@cubic-dev-ai cubic-dev-ai 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.

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/@supports inside 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);

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.

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>

@dielduarte dielduarte Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

@cubic-dev-ai
cubic-dev-ai Bot dismissed their stale review July 21, 2026 13:12

Dismissed because Cubic found issues in a newer review.

@cubic-dev-ai cubic-dev-ai 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.

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.

@cubic-dev-ai cubic-dev-ai 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.

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

@cubic-dev-ai cubic-dev-ai 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.

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

@dielduarte
dielduarte marked this pull request as ready for review July 21, 2026 17:35
Comment thread packages/react-email/src/components/tailwind/utils/css/constants.ts
`);
});

// Tailwind >=4.3.3 changed the shape of variant utilities: instead of nesting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how about we just mention the issue's url here instead? like a

// see https://...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agree! I am hating all these comments, but trying to keep ones explaining why code exists the code itself it not enough

@cubic-dev-ai cubic-dev-ai 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.

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

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