Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-tailwind-media-query-compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-email/tailwind": patch
---

Convert modern CSS range media queries and nesting to legacy syntax for email client compatibility
24 changes: 12 additions & 12 deletions packages/tailwind/src/tailwind.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('Tailwind component', () => {
<meta name="x-apple-disable-message-reformatting" />
<!--$-->
<style>
.md_p-4{@media (width>=48rem){padding:1rem!important}}
@media (min-width:48rem){.md_p-4{padding:1rem!important}}
</style>
</head>
<body>
Expand Down Expand Up @@ -352,7 +352,7 @@ describe('Tailwind component', () => {
<meta name="x-apple-disable-message-reformatting" />
<!--$-->
<style>
.sm_bg-red-50{@media (width>=40rem){background-color:rgb(254,242,242)!important}}.sm_text-sm{@media (width>=40rem){font-size:0.875rem!important;line-height:1.4285714285714286!important}}.md_text-lg{@media (width>=48rem){font-size:1.125rem!important;line-height:1.5555555555555556!important}}
@media (min-width:40rem){.sm_bg-red-50{background-color:rgb(254,242,242)!important}}@media (min-width:40rem){.sm_text-sm{font-size:0.875rem!important;line-height:1.4285714285714286!important}}@media (min-width:48rem){.md_text-lg{font-size:1.125rem!important;line-height:1.5555555555555556!important}}
</style></head
><span
><!--[if mso]><i style="letter-spacing: 10px;mso-font-width:-100%;" hidden>&nbsp;</i><![endif]--></span
Expand Down Expand Up @@ -390,7 +390,7 @@ describe('Tailwind component', () => {
);

expect(actualOutput).toMatchInlineSnapshot(
`"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--><style>.text-body{@media (prefers-color-scheme:dark){color:orange!important}}</style></head><body class="text-body"><table border="0" width="100%" cellPadding="0" cellSpacing="0" role="presentation" align="center"><tbody><tr><td style="color:green">this is the body</td></tr></tbody></table><!--/$--></body></html>"`,
`"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/><!--$--><style>@media (prefers-color-scheme:dark){.text-body{color:orange!important}}</style></head><body class="text-body"><table border="0" width="100%" cellPadding="0" cellSpacing="0" role="presentation" align="center"><tbody><tr><td style="color:green">this is the body</td></tr></tbody></table><!--/$--></body></html>"`,
);
});

Expand Down Expand Up @@ -425,7 +425,7 @@ describe('Tailwind component', () => {
<meta name="x-apple-disable-message-reformatting" />
<!--$-->
<style>
.xl_bg-green-500{@media (width>=1280px){background-color:rgb(0,201,80)!important}}.twoxl_bg-blue-500{@media (width>=1536px){background-color:rgb(43,127,255)!important}}
@media (min-width:1280px){.xl_bg-green-500{background-color:rgb(0,201,80)!important}}@media (min-width:1536px){.twoxl_bg-blue-500{background-color:rgb(43,127,255)!important}}
</style>
</head>
<div class="xl_bg-green-500" style="background-color:rgb(255,226,226)">
Expand Down Expand Up @@ -453,7 +453,7 @@ describe('Tailwind component', () => {
<head>
<!--$-->
<style>
.lg_max-h-calc50pxplus5rem{@media (width>=64rem){max-height:calc(50px + 5rem)!important}}
@media (min-width:64rem){.lg_max-h-calc50pxplus5rem{max-height:calc(50px + 5rem)!important}}
</style>
</head>
<div
Expand Down Expand Up @@ -496,7 +496,7 @@ describe('Tailwind component', () => {
<head>
<!--$-->
<style>
.sm_bg-red-300{@media (width>=40rem){background-color:rgb(255,162,162)!important}}.md_bg-red-400{@media (width>=48rem){background-color:rgb(255,100,103)!important}}.lg_bg-red-500{@media (width>=64rem){background-color:rgb(251,44,54)!important}}
@media (min-width:40rem){.sm_bg-red-300{background-color:rgb(255,162,162)!important}}@media (min-width:48rem){.md_bg-red-400{background-color:rgb(255,100,103)!important}}@media (min-width:64rem){.lg_bg-red-500{background-color:rgb(251,44,54)!important}}
</style>
</head>
<body>
Expand Down Expand Up @@ -525,7 +525,7 @@ describe('Tailwind component', () => {
</Tailwind>,
),
).toMatchInlineSnapshot(
`"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html lang="en"><head><!--$--><style>.sm_bg-red-300{@media (width>=40rem){background-color:rgb(255,162,162)!important}}.md_bg-red-400{@media (width>=48rem){background-color:rgb(255,100,103)!important}}.lg_bg-red-500{@media (width>=64rem){background-color:rgb(251,44,54)!important}}</style></head><body><div class="sm_bg-red-300 md_bg-red-400 lg_bg-red-500" style="background-color:rgb(255,201,201)"></div><!--/$--></body></html>"`,
`"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html lang="en"><head><!--$--><style>@media (min-width:40rem){.sm_bg-red-300{background-color:rgb(255,162,162)!important}}@media (min-width:48rem){.md_bg-red-400{background-color:rgb(255,100,103)!important}}@media (min-width:64rem){.lg_bg-red-500{background-color:rgb(251,44,54)!important}}</style></head><body><div class="sm_bg-red-300 md_bg-red-400 lg_bg-red-500" style="background-color:rgb(255,201,201)"></div><!--/$--></body></html>"`,
);
});

Expand Down Expand Up @@ -560,7 +560,7 @@ describe('Tailwind component', () => {
<head>
<!--$-->
<style>
.text-body{@media (width>=40rem){color:darkgreen!important}}
@media (min-width:40rem){.text-body{color:darkgreen!important}}
</style>
</head>
<body>
Expand Down Expand Up @@ -590,7 +590,7 @@ describe('Tailwind component', () => {
<head>
<!--$-->
<style>
.hover_bg-red-600{&:hover{@media (hover:hover){background-color:rgb(231,0,11)!important}}}.focus_bg-red-700{&:focus{background-color:rgb(193,0,7)!important}}.sm_bg-red-300{@media (width>=40rem){background-color:rgb(255,162,162)!important}}.sm_hover_bg-red-200{@media (width>=40rem){&:hover{@media (hover:hover){background-color:rgb(255,201,201)!important}}}}.md_bg-red-400{@media (width>=48rem){background-color:rgb(255,100,103)!important}}.lg_bg-red-500{@media (width>=64rem){background-color:rgb(251,44,54)!important}}
@media (hover:hover){.hover_bg-red-600:hover{background-color:rgb(231,0,11)!important}}.focus_bg-red-700:focus{background-color:rgb(193,0,7)!important}@media (min-width:40rem){.sm_bg-red-300{background-color:rgb(255,162,162)!important}}@media (min-width:40rem){@media (hover:hover){.sm_hover_bg-red-200:hover{background-color:rgb(255,201,201)!important}}}@media (min-width:48rem){.md_bg-red-400{background-color:rgb(255,100,103)!important}}@media (min-width:64rem){.lg_bg-red-500{background-color:rgb(251,44,54)!important}}
</style>
</head>
<body>
Expand Down Expand Up @@ -661,7 +661,7 @@ describe('Tailwind component', () => {
<meta name="x-apple-disable-message-reformatting" />
<!--$-->
<style>
.max-sm_text-red-600{@media (width<40rem){color:rgb(231,0,11)!important}}
@media (max-width:40rem){.max-sm_text-red-600{color:rgb(231,0,11)!important}}
</style>
</head>
<p class="max-sm_text-red-600" style="color:rgb(20,71,230)">I am some text</p>
Expand Down Expand Up @@ -705,7 +705,7 @@ describe('Tailwind component', () => {
<head>
<!--$-->
<style>
.sm_bg-red-500{@media (width>=40rem){background-color:rgb(251,44,54)!important}}
@media (min-width:40rem){.sm_bg-red-500{background-color:rgb(251,44,54)!important}}
</style>
<style></style>
<link />
Expand Down Expand Up @@ -926,7 +926,7 @@ describe('Tailwind component', () => {
<head>
<!--$-->
<style>
.sm_border-custom{@media (width>=40rem){border:2px solid!important}}
@media (min-width:40rem){.sm_border-custom{border:2px solid!important}}
</style>
</head>
<body>
Expand Down
2 changes: 2 additions & 0 deletions packages/tailwind/src/tailwind.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from 'react';
import type { Config } from 'tailwindcss';
import { useSuspensedPromise } from './hooks/use-suspended-promise';
import { sanitizeStyleSheet } from './sanitize-stylesheet';
import { downlevelCss } from './utils/css/downlevel-css';
import { extractRulesPerClass } from './utils/css/extract-rules-per-class';
import { getCustomProperties } from './utils/css/get-custom-properties';
import { sanitizeNonInlinableRules } from './utils/css/sanitize-non-inlinable-rules';
Expand Down Expand Up @@ -118,6 +119,7 @@ export function Tailwind({ children, config }: TailwindProps) {
),
};
sanitizeNonInlinableRules(nonInlineStyles);
downlevelCss(nonInlineStyles);

const hasNonInlineStylesToApply = nonInlinableRules.size > 0;
let appliedNonInlineStyles = false as boolean;
Expand Down
78 changes: 78 additions & 0 deletions packages/tailwind/src/utils/css/downlevel-css.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { generate, parse, type StyleSheet } from 'css-tree';
import { downlevelCss } from './downlevel-css';

function transform(css: string): string {
const ast = parse(css) as StyleSheet;
downlevelCss(ast);
return generate(ast);
}

describe('downlevelCss()', () => {
describe('range media feature conversion', () => {
it('converts width>= to min-width', () => {
expect(transform('@media (width>=40rem){.foo{padding:1rem}}')).toBe(
'@media (min-width:40rem){.foo{padding:1rem}}',
);
});

it('converts width<= to max-width', () => {
expect(transform('@media (width<=40rem){.foo{padding:1rem}}')).toBe(
'@media (max-width:40rem){.foo{padding:1rem}}',
);
});

it('does not modify non-range media features', () => {
expect(transform('@media (hover:hover){.foo{color:red}}')).toBe(
'@media (hover:hover){.foo{color:red}}',
);
});
});

describe('CSS unnesting', () => {
it('hoists @media out of rules', () => {
expect(
transform('.sm_p-4{@media (width>=40rem){padding:1rem!important}}'),
).toBe('@media (min-width:40rem){.sm_p-4{padding:1rem!important}}');
});

it('resolves & nesting selector with pseudo class', () => {
expect(transform('.foo{&:hover{color:red}}')).toBe(
'.foo:hover{color:red}',
);
});

it('handles @media + & combined', () => {
expect(
transform(
'.sm_focus_outline-none{@media (width>=40rem){&:focus{outline-style:none!important}}}',
),
).toBe(
'@media (min-width:40rem){.sm_focus_outline-none:focus{outline-style:none!important}}',
);
});

it('handles triple nesting: @media > &:hover > @media (hover:hover)', () => {
expect(
transform(
'.sm_hover_bg-red{@media (width>=40rem){&:hover{@media (hover:hover){background-color:red!important}}}}',
),
).toBe(
'@media (min-width:40rem){@media (hover:hover){.sm_hover_bg-red:hover{background-color:red!important}}}',
);
});

it('leaves plain rules unchanged', () => {
expect(transform('.foo{padding:1rem}')).toBe('.foo{padding:1rem}');
});

it('handles multiple rules', () => {
expect(
transform(
'.a{@media (width>=40rem){padding:1rem!important}}.b{@media (width>=48rem){margin:2rem!important}}',
),
).toBe(
'@media (min-width:40rem){.a{padding:1rem!important}}@media (min-width:48rem){.b{margin:2rem!important}}',
);
});
});
});
Loading
Loading