diff --git a/.changeset/tailwind-theme-vars-in-utility-variables.md b/.changeset/tailwind-theme-vars-in-utility-variables.md
new file mode 100644
index 0000000000..581bc4b975
--- /dev/null
+++ b/.changeset/tailwind-theme-vars-in-utility-variables.md
@@ -0,0 +1,5 @@
+---
+"react-email": patch
+---
+
+`` now resolves theme values that utilities pass through their own variables, so gradients (`from-red-500`), shadow and ring colors (`shadow-red-500`, `ring-blue-500`), `blur-*`, `drop-shadow-*` and `translate-*` no longer inline an unresolvable `var(--color-…)` or `var(--spacing)`.
diff --git a/packages/react-email/src/components/tailwind/tailwind.spec.tsx b/packages/react-email/src/components/tailwind/tailwind.spec.tsx
index 5268011e8e..a9f80b7580 100644
--- a/packages/react-email/src/components/tailwind/tailwind.spec.tsx
+++ b/packages/react-email/src/components/tailwind/tailwind.spec.tsx
@@ -134,6 +134,32 @@ describe('Tailwind component', () => {
expect(defaultElement?.style.boxShadow).not.toContain('rgb(255,0,0');
});
+ it('resolves theme values passed through utility variables', async () => {
+ const html = await render(
+
+
+
+ gradient
+
+
+ colored
+
+ transformed
+ ,
+ );
+ const document = new DOMParser().parseFromString(html, 'text/html');
+ const [gradient, colored, transformed] = document.querySelectorAll('div');
+
+ expect(html).not.toContain('var(');
+ expect(gradient?.getAttribute('style')).toContain(
+ 'linear-gradient(to right,rgb(251,44,54) 0%,rgb(0,201,80) 50%,rgb(43,127,255) 100%)',
+ );
+ expect(colored?.getAttribute('style')).toContain('rgb(251,44,54');
+ expect(colored?.getAttribute('style')).toContain('rgb(43,127,255)');
+ expect(transformed?.getAttribute('style')).toContain('filter:blur(8px)');
+ expect(transformed?.getAttribute('style')).toContain('translate:0.5rem 0');
+ });
+
it('does not use hover shadow colors for the inline default', async () => {
const html = await render(
diff --git a/packages/react-email/src/components/tailwind/utils/css/resolve-all-css-variables.spec.ts b/packages/react-email/src/components/tailwind/utils/css/resolve-all-css-variables.spec.ts
index 06bea30be1..11ec394de4 100644
--- a/packages/react-email/src/components/tailwind/utils/css/resolve-all-css-variables.spec.ts
+++ b/packages/react-email/src/components/tailwind/utils/css/resolve-all-css-variables.spec.ts
@@ -188,6 +188,25 @@ describe('resolveAllCSSVariables', () => {
);
});
+ it('resolves variables used inside custom property definitions', () => {
+ const root = parse(`:root {
+ --color-red-500: red;
+ --blur-sm: 8px;
+}
+.from-red-500 {
+ --tw-gradient-from: var(--color-red-500);
+ --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, transparent);
+}
+.blur-sm {
+ --tw-blur: blur(var(--blur-sm));
+ filter: var(--tw-blur,) var(--tw-drop-shadow,);
+}`);
+ resolveAllCssVariables(root);
+ expect(generate(root)).toMatchInlineSnapshot(
+ `":root{--color-red-500: red;--blur-sm: 8px}.from-red-500{--tw-gradient-from:red;--tw-gradient-stops:red,var(--tw-gradient-to, transparent)}.blur-sm{--tw-blur:blur(8px);filter:blur(8px) var(--tw-drop-shadow,)}"`,
+ );
+ });
+
it('uses fallback values when variable definition is not found', () => {
const root = parse(`.box {
width: var(--undefined-width, 150px);
diff --git a/packages/react-email/src/components/tailwind/utils/css/resolve-all-css-variables.ts b/packages/react-email/src/components/tailwind/utils/css/resolve-all-css-variables.ts
index 347d6755b1..e09e9809a1 100644
--- a/packages/react-email/src/components/tailwind/utils/css/resolve-all-css-variables.ts
+++ b/packages/react-email/src/components/tailwind/utils/css/resolve-all-css-variables.ts
@@ -23,7 +23,6 @@ export interface VariableDefinition {
declaration: Declaration;
path: CssNode[];
variableName: string;
- definition: string;
}
function doSelectorsIntersect(
@@ -96,43 +95,50 @@ export function resolveAllCssVariables(node: CssNode) {
declaration,
path: [...path],
variableName: declaration.property,
- definition: generate(declaration.value),
});
- } else {
- function parseVariableUsesFrom(node: CssNode) {
- walk(node, {
- visit: 'Function',
- enter(funcNode) {
- if (funcNode.name === 'var') {
- const children = funcNode.children.toArray();
- const name = generate(children[0]!);
- const fallback =
- // The second argument should be an "," Operator Node,
- // such that the actual fallback is only in the third argument
- children[2] ? generate(children[2]) : undefined;
-
- variableUses.add({
- declaration,
- path: [...path],
- fallback,
- variableName: name,
- raw: generate(funcNode),
- });
+ }
- if (fallback?.includes('var(')) {
- const parsedFallback = parse(fallback, {
- context: 'value',
- });
+ // Custom properties can reference other variables too, e.g. Tailwind's
+ // `--tw-gradient-from: var(--color-red-500)`, which is only read later
+ // through `var(--tw-gradient-from)` when inlining. css-tree keeps their
+ // values as Raw, so they need parsing before their var()s can be found.
+ function parseVariableUsesFrom(node: CssNode) {
+ walk(node, {
+ visit: 'Function',
+ enter(funcNode) {
+ if (funcNode.name === 'var') {
+ const children = funcNode.children.toArray();
+ const name = generate(children[0]!);
+ const fallback =
+ // The second argument should be an "," Operator Node,
+ // such that the actual fallback is only in the third argument
+ children[2] ? generate(children[2]) : undefined;
+
+ variableUses.add({
+ declaration,
+ path: [...path],
+ fallback,
+ variableName: name,
+ raw: generate(funcNode),
+ });
+
+ if (fallback?.includes('var(')) {
+ const parsedFallback = parse(fallback, {
+ context: 'value',
+ });
- parseVariableUsesFrom(parsedFallback);
- }
+ parseVariableUsesFrom(parsedFallback);
}
- },
- });
- }
-
- parseVariableUsesFrom(declaration.value);
+ }
+ },
+ });
}
+
+ parseVariableUsesFrom(
+ declaration.value.type === 'Raw'
+ ? parse(declaration.value.value, { context: 'value' })
+ : declaration.value,
+ );
}
path.unshift(node);
},
@@ -158,7 +164,7 @@ export function resolveAllCssVariables(node: CssNode) {
use.declaration.value = parse(
generate(use.declaration.value).replaceAll(
use.raw,
- definition.definition,
+ generate(definition.declaration.value),
),
{
context: 'value',
@@ -178,7 +184,7 @@ export function resolveAllCssVariables(node: CssNode) {
use.declaration.value = parse(
generate(use.declaration.value).replaceAll(
use.raw,
- definition.definition,
+ generate(definition.declaration.value),
),
{
context: 'value',
@@ -215,7 +221,7 @@ export function resolveAllCssVariables(node: CssNode) {
use.declaration.value = parse(
generate(use.declaration.value).replaceAll(
use.raw,
- definition.definition,
+ generate(definition.declaration.value),
),
{
context: 'value',
@@ -226,7 +232,14 @@ export function resolveAllCssVariables(node: CssNode) {
}
}
- if (!hasReplaced && use.fallback) {
+ // A custom property's own fallback is left in place, since a declaration
+ // on the element's other classes might still define the variable when
+ // the styles are inlined.
+ if (
+ !hasReplaced &&
+ use.fallback &&
+ !use.declaration.property.startsWith('--')
+ ) {
use.declaration.value = parse(
generate(use.declaration.value).replaceAll(use.raw, use.fallback),
{ context: 'value' },