Skip to content

Commit 5e004e7

Browse files
Fix bug with nested @apply rules in utility classes (#17924)
## Bug When one utility applied another utility that contained pseudo-selectors (like :hover, :disabled), the pseudo-selectors were not properly carried through the dependency chain. This was because we were only tracking dependencies on the immediate parent rather than all parents in the path. ## Fix - Modified the dependency resolution to track dependencies through the entire parent path - Rewrote the node replacement logic to use the more robust walk() function - These changes ensure pseudo-selectors are properly preserved when utilities apply other utilities ## Test Plan - Run `vitest run packages/tailwindcss/src/index.test.ts` to verify the new test case passes - The test case verifies that when utility 'test2' applies utility 'test' with hover/disabled states, all pseudo-selectors are correctly preserved in the output CSS
1 parent 179e5dd commit 5e004e7

File tree

3 files changed

+92
-8
lines changed

3 files changed

+92
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Fix incorrectly replacing `_` with ` ` in arbitrary modifier shorthand `bg-red-500/(--my_opacity)` ([#17889](https://github.com/tailwindlabs/tailwindcss/pull/17889))
2121
- Upgrade: Bump dependencies in parallel and make the upgrade faster ([#17898](https://github.com/tailwindlabs/tailwindcss/pull/17898))
2222
- Don't scan `.log` files for classes by default ([#17906](https://github.com/tailwindlabs/tailwindcss/pull/17906))
23+
- Ensure that custom utilities applying other custom utilities don't swallow nested `@apply` rules ([#17925](https://github.com/tailwindlabs/tailwindcss/pull/17925))
2324

2425
## [4.1.5] - 2025-04-30
2526

packages/tailwindcss/src/apply.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
2323
let definitions = new DefaultMap(() => new Set<AstNode>())
2424

2525
// Collect all new `@utility` definitions and all `@apply` rules first
26-
walk([root], (node, { parent }) => {
26+
walk([root], (node, { parent, path }) => {
2727
if (node.kind !== 'at-rule') return
2828

2929
// Do not allow `@apply` rules inside `@keyframes` rules.
@@ -66,7 +66,12 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
6666
parents.add(parent)
6767

6868
for (let dependency of resolveApplyDependencies(node, designSystem)) {
69-
dependencies.get(parent).add(dependency)
69+
// Mark every parent in the path as having a dependency to that utility.
70+
for (let parent of path) {
71+
if (parent === node) continue
72+
if (!parents.has(parent)) continue
73+
dependencies.get(parent).add(dependency)
74+
}
7075
}
7176
}
7277
})
@@ -151,11 +156,10 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
151156
for (let parent of sorted) {
152157
if (!('nodes' in parent)) continue
153158

154-
for (let i = 0; i < parent.nodes.length; i++) {
155-
let node = parent.nodes[i]
156-
if (node.kind !== 'at-rule' || node.name !== '@apply') continue
159+
walk(parent.nodes, (child, { replaceWith }) => {
160+
if (child.kind !== 'at-rule' || child.name !== '@apply') return
157161

158-
let candidates = node.params.split(/\s+/g)
162+
let candidates = child.params.split(/\s+/g)
159163

160164
// Replace the `@apply` rule with the actual utility classes
161165
{
@@ -181,10 +185,11 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
181185
}
182186
}
183187

184-
parent.nodes.splice(i, 1, ...newNodes)
188+
replaceWith(newNodes)
185189
}
186-
}
190+
})
187191
}
192+
188193
return features
189194
}
190195

packages/tailwindcss/src/index.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,84 @@ describe('@apply', () => {
612612
}"
613613
`)
614614
})
615+
616+
// https://github.com/tailwindlabs/tailwindcss/issues/17924
617+
it('should correctly apply nested usages of @apply when one @utility applies another', async () => {
618+
expect(
619+
await compileCss(
620+
css`
621+
@theme {
622+
--color-green-500: green;
623+
--color-red-500: red;
624+
--color-indigo-500: indigo;
625+
}
626+
627+
@tailwind utilities;
628+
629+
@utility test2 {
630+
@apply test;
631+
}
632+
633+
@utility test {
634+
@apply bg-green-500;
635+
&:hover {
636+
@apply bg-red-500;
637+
}
638+
&:disabled {
639+
@apply bg-indigo-500;
640+
}
641+
}
642+
643+
.foo {
644+
@apply test2;
645+
}
646+
`,
647+
['foo', 'test', 'test2'],
648+
),
649+
).toMatchInlineSnapshot(`
650+
":root, :host {
651+
--color-green-500: green;
652+
--color-red-500: red;
653+
--color-indigo-500: indigo;
654+
}
655+
656+
.test {
657+
background-color: var(--color-green-500);
658+
}
659+
660+
.test:hover {
661+
background-color: var(--color-red-500);
662+
}
663+
664+
.test:disabled {
665+
background-color: var(--color-indigo-500);
666+
}
667+
668+
.test2 {
669+
background-color: var(--color-green-500);
670+
}
671+
672+
.test2:hover {
673+
background-color: var(--color-red-500);
674+
}
675+
676+
.test2:disabled {
677+
background-color: var(--color-indigo-500);
678+
}
679+
680+
.foo {
681+
background-color: var(--color-green-500);
682+
}
683+
684+
.foo:hover {
685+
background-color: var(--color-red-500);
686+
}
687+
688+
.foo:disabled {
689+
background-color: var(--color-indigo-500);
690+
}"
691+
`)
692+
})
615693
})
616694

617695
describe('arbitrary variants', () => {

0 commit comments

Comments
 (0)