Repository navigation
fix(tailwind): downlevel CSS for email client compatibility - #3086
gabrielmfern merged 10 commits into
Conversation
🦋 Changeset detectedLatest 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 |
|
@Ouranos27 is attempting to deploy a commit to the resend Team on Vercel. A member of the Team first needs to authorize it. |
commit: |
There was a problem hiding this comment.
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:parseBlockSegmentsnot splitting declarations before nested@mediacan 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-aiwith guidance or docs links (includingllms.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.
|
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 Declarations before nested @media: React Email's pipeline separates inlinable declarations (inlined into elements) from non-inlinable ones (kept in |
2216625 to
89eafb3
Compare
|
@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! |
There was a problem hiding this comment.
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
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
39dc027 to
deb0ce3
Compare
Signed-off-by: gabriel miranda <gabriel@resend.com>
Signed-off-by: gabriel miranda <gabriel@resend.com>
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
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:
2. CSS Nesting — email clients don't parse nested at-rules inside selectors:
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:(width>=40rem)→(min-width:40rem), handles>=,<=,>,<for bothwidthandheight@mediarules to the top level, wrapping the parent selector insideThe transform is applied in
tailwind.tsxaftergenerate(nonInlineStyles), so it only affects the CSS that goes into the<style>tag — inlined styles are not affected.What's not covered
&:hovernesting (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.</>are approximated as<=/>=(sub-pixel difference, irrelevant for email).Testing
&:hoverpass-through, dark mode, and edge casestailwind.spec.tsxwill 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
(width|height >=|<=|>|<)to legacymin-/max-media features.@media/@supportsto top level; only affects non-inlinable<style>CSS.Refactors
css-treeAST withwalk; applied viadownlevelForEmailClients()intailwind.tsx.css-treetypes (FeatureRange/Feature) to remove casts; added unit tests and updated snapshots.Written for commit 955954a. Summary will update on new commits.