Skip to content

Commit 698f962

Browse files
fix(tailwind): pseudo selectors (#2963)
Co-authored-by: gabriel miranda <gabrielmfern@outlook.com>
1 parent 1360dd9 commit 698f962

File tree

3 files changed

+53
-1
lines changed

3 files changed

+53
-1
lines changed

.changeset/new-moles-fry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-email/tailwind": patch
3+
---
4+
5+
fix mixed pseudo selectors not being split

packages/tailwind/src/utils/css/extract-rules-per-class.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,35 @@ describe('extractRulesPerClass()', async () => {
112112
}
113113
`);
114114
});
115+
116+
it('treats rules with pseudo-selectors as fully non-inlinable', async () => {
117+
const tailwind = await setupTailwind({
118+
plugins: [
119+
{
120+
handler: (api) => {
121+
api.addUtilities({
122+
'.btn:hover': {
123+
color: 'red',
124+
},
125+
});
126+
},
127+
},
128+
],
129+
});
130+
const classes = ['btn'];
131+
tailwind.addUtilities(classes);
132+
133+
const stylesheet = tailwind.getStyleSheet();
134+
const { inlinable, nonInlinable } = extractRulesPerClass(
135+
stylesheet,
136+
classes,
137+
);
138+
139+
expect(convertToComparable(inlinable)).toMatchInlineSnapshot('{}');
140+
expect(convertToComparable(nonInlinable)).toMatchInlineSnapshot(`
141+
{
142+
"btn": ".btn{&:hover{color:red}}",
143+
}
144+
`);
145+
});
115146
});

packages/tailwind/src/utils/css/split-mixed-rule.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type CssNode, clone, List, type Rule } from 'css-tree';
1+
import { type CssNode, clone, find, List, type Rule } from 'css-tree';
22
import { isPartInlinable } from './is-part-inlinable';
33

44
/**
@@ -12,6 +12,22 @@ export function splitMixedRule(rule: Rule): {
1212
inlinablePart: Rule | null;
1313
nonInlinablePart: Rule | null;
1414
} {
15+
// If the selector itself contains pseudo-selectors, every declaration in the
16+
// block only applies in that pseudo context — splitting would incorrectly
17+
// inline them as base styles. Return the whole rule as non-inlinable.
18+
const selectorHasPseudo =
19+
rule.prelude !== null &&
20+
find(
21+
rule.prelude,
22+
(node) =>
23+
node.type === 'PseudoClassSelector' ||
24+
node.type === 'PseudoElementSelector',
25+
) !== null;
26+
27+
if (selectorHasPseudo) {
28+
return { inlinablePart: null, nonInlinablePart: clone(rule) as Rule };
29+
}
30+
1531
const ruleCloneInlinable = clone(rule) as Rule;
1632
const ruleCloneNonInlinable = clone(rule) as Rule;
1733

0 commit comments

Comments
 (0)