Skip to content

fix(tailwind): downlevel CSS for email client compatibility - #3086

Merged
gabrielmfern merged 10 commits into
resend:canaryfrom
Ouranos27:fix/downlevel-media-queries-for-email-clients
May 4, 2026
Merged

gabrielmfern merged 10 commits into
resend:canaryfrom
Ouranos27:fix/downlevel-media-queries-for-email-clients

Conversation

@Ouranos27

@Ouranos27 Ouranos27 commented Mar 18, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Tailwind CSS v4's compile().build() generates two modern CSS features that most email clients don't support:

1. Media Queries Level 4 range syntax — Gmail, Outlook, Yahoo strip these entirely:

/* Tailwind v4 output — broken in Gmail */
@media (width>=40rem) { ... }

/* What email clients need */
@media (min-width:40rem) { ... }

2. CSS Nesting — email clients don't parse nested at-rules inside selectors:

/* Tailwind v4 output — broken in Gmail */
.sm_p-4{@media (width>=40rem){padding:1rem!important}}

/* What email clients need */
@media (min-width:40rem){.sm_p-4{padding:1rem!important}}

When React Email uses the Tailwind compiler API directly, the PostCSS/Lightning CSS pipeline that normally downlevels these features doesn't run. The result: responsive breakpoints (sm:, md:, lg:, max-sm:) are silently stripped by email clients.

Solution

Adds a downlevelForEmailClients() string transform that runs on the generated non-inlinable CSS (the <style> tag content) before injection:

  1. Range syntax → legacy: (width>=40rem) → (min-width:40rem), handles >=, <=, >, < for both width and height
  2. Unnest @media: moves nested @media rules to the top level, wrapping the parent selector inside

The transform is applied in tailwind.tsx after generate(nonInlineStyles), so it only affects the CSS that goes into the <style> tag — inlined styles are not affected.

What's not covered

  • &:hover nesting (e.g. .hover_bg-red{&:hover{@media (hover:hover){...}}}) — this is preserved as-is. Pseudo-class support in email clients is limited regardless, and resolving & to the parent selector is a separate concern.
  • Strict < / > are approximated as <= / >= (sub-pixel difference, irrelevant for email).

Testing

  • 15 unit tests covering range syntax conversion, unnesting, multi-@media per selector, &:hover pass-through, dark mode, and edge cases
  • Integration test snapshots in tailwind.spec.tsx will need updating (the <style> output changes from nested/range to legacy/unnested) — I couldn't run them locally since the workspace packages need a full build. Happy to update once CI runs.

Fixes #2712

References


Summary by cubic

Downlevels Tailwind v4 CSS for email clients by converting range media queries and unnesting nested at-rules before injection. Restores responsive and dark‑mode styles in Gmail, Outlook, and Yahoo.

  • Bug Fixes

    • Converts (width|height >=|<=|>|<) to legacy min-/max- media features.
    • Unnests nested @media/@supports to top level; only affects non-inlinable <style> CSS.
  • Refactors

    • Rewrote the transform using the css-tree AST with walk; applied via downlevelForEmailClients() in tailwind.tsx.
    • Augmented css-tree types (FeatureRange/Feature) to remove casts; added unit tests and updated snapshots.

Written for commit 955954a. Summary will update on new commits.

@changeset-bot

changeset-bot Bot commented Mar 18, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 955954a

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

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 Mar 18, 2026

Copy link
Copy Markdown
Contributor

@Ouranos27 is attempting to deploy a commit to the resend Team on Vercel.

A member of the Team first needs to authorize it.

@pkg-pr-new

pkg-pr-new Bot commented Mar 18, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 955954a

@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.

2 issues found across 3 files

Confidence score: 3/5

  • There is meaningful regression risk in packages/tailwind/src/utils/css/downlevel-for-email-clients.ts: parseBlockSegments not splitting declarations before nested @media can leave unsupported nested media untransformed for email clients.
  • Brace matching in the same file currently ignores CSS strings/comments, so quoted braces may break block boundary detection and produce malformed transformed CSS, which is user-facing output risk.
  • Given both findings are medium severity (6/10) with high confidence, this is likely fixable but not quite low-risk yet.
  • Pay close attention to packages/tailwind/src/utils/css/downlevel-for-email-clients.ts - parsing and brace-boundary logic can silently generate incorrect CSS output.
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/tailwind/src/utils/css/downlevel-for-email-clients.ts">

<violation number="1" location="packages/tailwind/src/utils/css/downlevel-for-email-clients.ts:45">
P2: Brace matching ignores CSS strings/comments, so quoted braces can cause incorrect block boundaries and malformed transformed CSS.</violation>

<violation number="2" location="packages/tailwind/src/utils/css/downlevel-for-email-clients.ts:70">
P2: `parseBlockSegments` fails to split declarations before nested `@media`, causing `unnestMediaQueries` to miss and preserve unsupported nested media.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/tailwind/src/utils/css/downlevel-for-email-clients.ts Outdated
Comment thread packages/tailwind/src/utils/css/downlevel-for-email-clients.ts Outdated
@Ouranos27

Copy link
Copy Markdown
Contributor Author

Both findings are about edge cases that can't occur in this context:

Brace matching (strings/comments): The input is always the output of css-tree's generate(), which produces minified CSS with no comments or quoted braces. No need for a full CSS tokenizer here.

Declarations before nested @media: React Email's pipeline separates inlinable declarations (inlined into elements) from non-inlinable ones (kept in <style>) before this function runs. The block content only ever contains @media rules, never a mix of bare declarations and @media.

@gabrielmfern
gabrielmfern force-pushed the fix/downlevel-media-queries-for-email-clients branch from 2216625 to 89eafb3 Compare March 25, 2026 20:25
@Ouranos27
Ouranos27 requested a review from gabrielmfern March 29, 2026 13:03
@Ouranos27

Copy link
Copy Markdown
Contributor Author

@gabrielmfern Rewrote the implementation using css-tree AST as you suggested, no more regex. Also fixed lint/formatting issues, updated all snapshots, and added a changeset.

Ready for another look when you get a chance!

Comment thread .changeset/downlevel-email-css.md Outdated

@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: This PR introduces non-trivial CSS AST transformation logic to downlevel Tailwind v4 features. While it fixes compatibility issues, the complexity of AST manipulation on the core styling path warrants

Ouranos27 and others added 4 commits May 4, 2026 11:37
Tailwind CSS v4 generates modern CSS features that most email clients
don't support:

1. Media Queries Level 4 range syntax: `@media (width>=40rem)` —
   Gmail, Outlook, Yahoo strip these entirely.
2. CSS Nesting: `.class{@media (cond){decls}}` — email clients don't
   parse nested at-rules inside selectors.

This adds a `downlevelForEmailClients()` transform that runs on the
generated non-inlinable CSS before it's injected into the <style> tag:

- Converts range syntax to legacy min-width/max-width
- Unnests @media rules from inside selectors to top-level

Fixes resend#2712

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces regex-based approach with css-tree AST operations:
- Unnesting: walks Rules, collects nested Atrules, uses List.replace()
- Range syntax: walks FeatureRange nodes, replaces with Feature nodes
@gabrielmfern
gabrielmfern force-pushed the fix/downlevel-media-queries-for-email-clients branch from 39dc027 to deb0ce3 Compare May 4, 2026 14:41
Signed-off-by: gabriel miranda <gabriel@resend.com>
Signed-off-by: gabriel miranda <gabriel@resend.com>
Signed-off-by: gabriel miranda <gabriel@resend.com>

@gabrielmfern gabrielmfern left a comment

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.

thank you!

@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: Non-trivial CSS AST manipulation for downleveling Tailwind v4 features. The unnesting and range-syntax logic is complex and significantly impacts core CSS generation.

Signed-off-by: gabriel miranda <gabriel@resend.com>

@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 Tailwind v4 compatibility for email clients by downleveling modern CSS features (range syntax and nesting) using AST transformations with comprehensive test coverage.

Signed-off-by: gabriel miranda <gabriel@resend.com>
Signed-off-by: gabriel miranda <gabriel@resend.com>

@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: This PR introduces significant logic for CSS AST transformation and unnesting. Given the complexity of AST manipulation and the impact on email styling, it requires human review.

@gabrielmfern
gabrielmfern merged commit 87a2486 into resend:canary May 4, 2026
9 of 12 checks passed
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.

Tailwind breakpoints don't work in Gmail

2 participants