Skip to content

fix(tailwind): convert modern CSS to legacy syntax for email client compatibility#3144

Open
wesleyramalho wants to merge 3 commits intoresend:canaryfrom
wesleyramalho:fix/tailwind-media-query-compat
Open

fix(tailwind): convert modern CSS to legacy syntax for email client compatibility#3144
wesleyramalho wants to merge 3 commits intoresend:canaryfrom
wesleyramalho:fix/tailwind-media-query-compat

Conversation

@wesleyramalho
Copy link
Copy Markdown

@wesleyramalho wesleyramalho commented Apr 1, 2026

Summary

  • Tailwind v4 outputs CSS with range media queries (width>=40rem) and CSS nesting that email clients like Gmail on Android don't support
  • Adds a downlevelCss transformation step that converts range syntax to min-width/max-width and unnests rules to top-level @media blocks
  • Fixes the CSS output to use legacy-compatible syntax that works across all email clients

Closes #3009

Before

.sm_p-4{@media (width>=48rem){padding:1rem!important}}
.hover_bg-red-600{&:hover{@media (hover:hover){background-color:red!important}}}

After

@media (min-width:48rem){.sm_p-4{padding:1rem!important}}
@media (hover:hover){.hover_bg-red-600:hover{background-color:red!important}}

Test plan

  • New unit tests for downlevelCss() covering range conversion, unnesting, & resolution, triple nesting, and passthrough (9 tests)
  • All existing tailwind tests pass (12 snapshots updated)
  • Build succeeds
  • Lint passes

Summary by cubic

Downlevels Tailwind v4 CSS to legacy syntax for email clients by converting range media queries and unnesting rules. Adds a downlevelCss step so styles render correctly in Gmail on Android and similar clients.

  • Bug Fixes
    • Added downlevelCss to @react-email/tailwind to convert range media features to min-width/max-width.
    • Hoists nested @media and resolves & into top-level rules, preserving selector token order.
    • Added unit tests for range conversion, unnesting, and complex nesting; updated snapshots.
    • Declared local css-tree v3 node interfaces and typed walk callbacks to satisfy TypeScript.

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

…ompatibility

Tailwind v4 outputs CSS with range media queries (width>=40rem) and
CSS nesting that email clients like Gmail on Android don't support.
This adds a downlevelCss transformation step that converts range syntax
to min-width/max-width and unnests rules to top-level @media blocks.

Closes resend#3009
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 1, 2026

🦋 Changeset detected

Latest commit: c958bed

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/tailwind Patch
@react-email/components Patch
playground 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
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 1, 2026

@wesleyramalho 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 Apr 1, 2026

Open in StackBlitz

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

commit: 9319e14

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

Confidence score: 2/5

  • There is a high regression risk in packages/tailwind/src/utils/css/downlevel-css.ts: resolving & appears to insert parent selector tokens in reverse order, which can change selector semantics (for example, descendant relationships flipping).
  • Nesting expansion in packages/tailwind/src/utils/css/downlevel-css.ts reportedly uses only the first selector from a parent SelectorList, which can silently drop intended selectors and produce incomplete CSS output.
  • Given the medium-high severities (7/10 and 6/10) with strong confidence, this is likely user-facing behavior breakage rather than a minor cleanup issue.
  • Pay close attention to packages/tailwind/src/utils/css/downlevel-css.ts - selector ordering and SelectorList expansion may generate incorrect or missing selectors.
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-css.ts">

<violation number="1" location="packages/tailwind/src/utils/css/downlevel-css.ts:211">
P2: Nesting resolution drops selectors by expanding `&` with only the first selector from a parent `SelectorList`.</violation>

<violation number="2" location="packages/tailwind/src/utils/css/downlevel-css.ts:218">
P1: Resolving `&` inserts parent selector tokens in reverse order, which can change selector meaning (e.g., `.foo .bar` becoming `.bar .foo`).</violation>
</file>

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

- Fix P1: insert parent selector tokens in correct order (insertData
  before the & node instead of tracking prev pointer)
- Fix TS: define proper interfaces for css-tree v3 node types
  (FeatureRange, Feature) not yet in @types/css-tree@2.3.x
- Type walk callback parameters explicitly
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.

1 issue found across 1 file (changes from recent commits).

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-css.ts">

<violation number="1" location="packages/tailwind/src/utils/css/downlevel-css.ts:74">
P1: Nesting resolution only uses the first selector from a selector list, dropping additional parent selectors when replacing `&`.</violation>
</file>

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

@wesleyramalho
Copy link
Copy Markdown
Author

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@cubic-dev-ai can you review again?

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai bot commented Apr 1, 2026

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

...

@wesleyramalho I have started the AI code review. It will take a few minutes to complete.

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.

1 issue found across 5 files

Confidence score: 3/5

  • Nesting resolution in packages/tailwind/src/utils/css/downlevel-css.ts only expands & for the first selector, so multi-selector parents can lose selectors and change styling behavior.
  • This is a concrete, user-visible behavior change (severity 6/10), so there’s some regression risk despite being a single issue.
  • Pay close attention to packages/tailwind/src/utils/css/downlevel-css.ts - ensure & expansion preserves all parent selectors in selector lists.
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-css.ts">

<violation number="1" location="packages/tailwind/src/utils/css/downlevel-css.ts:267">
P2: Nesting resolution only expands `&` using the first selector in a selector list, so parent rules with multiple selectors (e.g., `.a, .b { &:hover { ... } }`) will drop additional selectors and change behavior.</violation>
</file>

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

@wesleyramalho wesleyramalho marked this pull request as ready for review April 1, 2026 12:39
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.

Media queries use nested syntax with unsupported ranges <= operators

1 participant