Skip to content

Tailwind breakpoints Gmail support#2976

Draft
gabrielmfern wants to merge 4 commits intocanaryfrom
cursor/tailwind-breakpoints-gmail-support-8622
Draft

Tailwind breakpoints Gmail support#2976
gabrielmfern wants to merge 4 commits intocanaryfrom
cursor/tailwind-breakpoints-gmail-support-8622

Conversation

@gabrielmfern
Copy link
Member

@gabrielmfern gabrielmfern commented Feb 25, 2026

feat(tailwind): Add email compatibility for Tailwind v4 CSS

This PR introduces a post-processing step to make Tailwind CSS v4 output compatible with Gmail.

Why this change?
Tailwind CSS v4 generates modern CSS features (e.g., CSS nesting, range syntax in media queries, rem units) that are not supported by Gmail, leading to broken email layouts.

What does it do?
A new makeStylesEmailCompatible utility is added to post-process the generated CSS. This function transforms the output by:

  • Unnesting @media rules.
  • Converting media query range syntax (e.g., (width>=40rem)) to legacy min-width/max-width (e.g., (min-width:640px)).
  • Converting rem units to px in media queries.
  • Resolving CSS nesting selectors (e.g., &:hover to .class:hover).
  • Grouping rules by media query for cleaner output.

Verification:

  • All existing snapshot tests have been updated and pass, reflecting the new Gmail-compatible output.
  • New dedicated unit tests for makeStylesEmailCompatible ensure the transformation logic is robust.
  • The full Tailwind package test suite passes.

Slack Thread

Open in Web Open in Cursor 


Summary by cubic

Make Tailwind v4 CSS Gmail-compatible by post-processing non-inlinable styles. Breakpoints, nested selectors, and media queries now render correctly in emails.

  • Bug Fixes
    • Unnests @media rules to top-level blocks.
    • Converts range syntax (width>=40rem, width<40rem) to min/max-width in px.
    • Converts rem to px in media queries (1rem = 16px), including legacy Feature nodes.
    • Resolves CSS nesting (& -> parent selector), including comma-separated selectors; preserves non-responsive queries (e.g., prefers-color-scheme).
    • Groups identical media queries into single @media blocks; fixes non-media node key collision.
    • Integrated via makeStylesEmailCompatible in Tailwind.tsx; added unit tests and updated snapshots.

Written for commit 4fa4965. Summary will update on new commits.

Tailwind CSS v4 generates modern CSS that Gmail doesn't support:
- CSS nesting: .class{@media{...}} instead of @media{.class{...}}
- Range syntax: (width>=40rem) instead of (min-width:640px)
- rem units in media queries instead of px

This adds a post-processing step (makeStylesEmailCompatible) that:
1. Unnests media queries from inside rules to the top level
2. Converts FeatureRange (width >= X) to Feature (min-width: X)
3. Converts rem to px in media query conditions (1rem = 16px)
4. Resolves CSS nesting selectors (& -> parent selector)
5. Groups rules sharing the same media query into single @media blocks

Fixes #2712

Co-authored-by: Gabriel Miranda <gabrielmfern@outlook.com>
@cursor
Copy link

cursor bot commented Feb 25, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@vercel
Copy link

vercel bot commented Feb 25, 2026

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

Project Deployment Actions Updated (UTC)
react-email Ready Ready Preview, Comment Feb 25, 2026 7:53pm
react-email-demo Ready Ready Preview, Comment Feb 25, 2026 7:53pm

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Feb 25, 2026

⚠️ No Changeset found

Latest commit: 4fa4965

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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 25, 2026

Open in StackBlitz

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

commit: 4fa4965

Co-authored-by: Gabriel Miranda <gabrielmfern@outlook.com>
Co-authored-by: Gabriel Miranda <gabrielmfern@outlook.com>
Copy link
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.

3 issues found across 4 files

Confidence score: 3/5

  • There is some user-visible risk: in packages/tailwind/src/utils/css/make-styles-email-compatible.ts, non-media rules can be overwritten when multiple top-level non-media nodes exist, dropping styles.
  • Selector rewriting can change which elements are matched when parent selectors are lists, e.g., .a, .b becomes .a, .b:hover rather than separate hover selectors.
  • Pay close attention to packages/tailwind/src/utils/css/make-styles-email-compatible.ts - grouping/selector handling and feature condition conversions may alter output CSS.
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/make-styles-email-compatible.ts">

<violation number="1" location="packages/tailwind/src/utils/css/make-styles-email-compatible.ts:144">
P2: `&` replacement breaks when the parent selector is a list (e.g., `.a, .b`), producing `.a, .b:hover` instead of `.a:hover, .b:hover`. This changes which elements are matched.</violation>

<violation number="2" location="packages/tailwind/src/utils/css/make-styles-email-compatible.ts:232">
P2: Only FeatureRange nodes are handled, so legacy `Feature` conditions like `(min-width: 40rem)` keep rem units and remain Gmail-incompatible. The rem-to-px conversion should also cover Feature nodes.</violation>

<violation number="3" location="packages/tailwind/src/utils/css/make-styles-email-compatible.ts:320">
P1: Non-media nodes are all grouped under the same key because result.length stays 0 inside the loop, so earlier non-media rules are overwritten. This drops rules when there are multiple top-level non-media nodes.</violation>
</file>

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

- Fix non-media node key collision in groupByMediaQuery (P1): used a
  separate counter instead of result.length which was always 0
- Convert rem to px in legacy Feature nodes too, not just FeatureRange (P2)
- Handle & replacement correctly with comma-separated parent selectors (P2)
- Add tests covering all three fixed issues

Co-authored-by: Gabriel Miranda <gabrielmfern@outlook.com>
Copy link
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 2 files (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/make-styles-email-compatible.ts">

<violation number="1" location="packages/tailwind/src/utils/css/make-styles-email-compatible.ts:202">
P2: Splitting the parent selector by `,` breaks valid selectors that include commas inside `:is()`/`:not()`/`:where()`; the nested selector will be corrupted. Consider parsing the selector list (top-level commas only) instead of a raw string split.</violation>
</file>

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

): string {
if (!nestedSelector.includes('&')) return nestedSelector;

const parentParts = parentSelector.split(',').map((s) => s.trim());
Copy link
Contributor

Choose a reason for hiding this comment

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

P2: Splitting the parent selector by , breaks valid selectors that include commas inside :is()/:not()/:where(); the nested selector will be corrupted. Consider parsing the selector list (top-level commas only) instead of a raw string split.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/tailwind/src/utils/css/make-styles-email-compatible.ts, line 202:

<comment>Splitting the parent selector by `,` breaks valid selectors that include commas inside `:is()`/`:not()`/`:where()`; the nested selector will be corrupted. Consider parsing the selector list (top-level commas only) instead of a raw string split.</comment>

<file context>
@@ -189,6 +189,26 @@ function resolveNestedRule(parentRule: Rule, nestedRule: Rule): CssNode[] {
+): string {
+  if (!nestedSelector.includes('&')) return nestedSelector;
+
+  const parentParts = parentSelector.split(',').map((s) => s.trim());
+  if (parentParts.length <= 1) {
+    return nestedSelector.replace(/&/g, parentSelector);
</file context>

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.

2 participants