Skip to content

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

Open
Ouranos27 wants to merge 2 commits intoresend:canaryfrom
Ouranos27:fix/downlevel-media-queries-for-email-clients
Open

fix(tailwind): downlevel CSS for email client compatibility#3086
Ouranos27 wants to merge 2 commits intoresend:canaryfrom
Ouranos27:fix/downlevel-media-queries-for-email-clients

Conversation

@Ouranos27
Copy link
Copy Markdown

@Ouranos27 Ouranos27 commented Mar 18, 2026

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 at-rules before the stylesheet is injected. Restores responsive styles in Gmail, Outlook, and Yahoo.

  • Bug Fixes

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

    • Rewrote the transform using the css-tree AST (replaces regex) for correctness and safer edge-case handling.

Written for commit 869f1ba. Summary will update on new commits.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 18, 2026

⚠️ No Changeset found

Latest commit: 869f1ba

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 18, 2026

@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
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 18, 2026

Open in StackBlitz

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

commit: 192189a

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

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.

@Ouranos27
Copy link
Copy Markdown
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.

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>
@gabrielmfern gabrielmfern force-pushed the fix/downlevel-media-queries-for-email-clients branch from 2216625 to 89eafb3 Compare March 25, 2026 20:25
@@ -0,0 +1,184 @@
/**
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.

we should not use regex here, we already use csstree, and regexes like this is going to be way too much to maintain.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Makes sense, I'll rewrite it using css-tree. I will push an update.

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