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/tailwind-theme-vars-in-utility-variables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-email": patch
---

`<Tailwind>` 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)`.
26 changes: 26 additions & 0 deletions packages/react-email/src/components/tailwind/tailwind.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Tailwind>
<Head />
<div className="bg-linear-to-r from-red-500 via-green-500 to-blue-500">
gradient
</div>
<div className="shadow-md shadow-red-500 ring-2 ring-blue-500">
colored
</div>
<div className="blur-sm translate-x-2">transformed</div>
</Tailwind>,
);
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(
<Tailwind>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export interface VariableDefinition {
declaration: Declaration;
path: CssNode[];
variableName: string;
definition: string;
}

function doSelectorsIntersect(
Expand Down Expand Up @@ -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);
},
Expand All @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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' },
Expand Down