Skip to content

Fix bug with nested @apply rules in utility classes (#17924) #17925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 8, 2025
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix incorrectly replacing `_` with ` ` in arbitrary modifier shorthand `bg-red-500/(--my_opacity)` ([#17889](https://github.com/tailwindlabs/tailwindcss/pull/17889))
- Upgrade: Bump dependencies in parallel and make the upgrade faster ([#17898](https://github.com/tailwindlabs/tailwindcss/pull/17898))
- Don't scan `.log` files for classes by default ([#17906](https://github.com/tailwindlabs/tailwindcss/pull/17906))
- Ensure that custom utilities applying other custom utilities don't swallow nested `@apply` rules ([#17925](https://github.com/tailwindlabs/tailwindcss/pull/17925))

## [4.1.5] - 2025-04-30

Expand Down
21 changes: 13 additions & 8 deletions packages/tailwindcss/src/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
let definitions = new DefaultMap(() => new Set<AstNode>())

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

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

for (let dependency of resolveApplyDependencies(node, designSystem)) {
dependencies.get(parent).add(dependency)
// Mark every parent in the path as having a dependency to that utility.
for (let parent of path) {
if (parent === node) continue
if (!parents.has(parent)) continue
dependencies.get(parent).add(dependency)
}
Comment on lines +69 to +74
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting, I knew we were performing a topological sort already so was confused why this didn't work.

But this makes sense, and tracking the full path also makes sense.

}
}
})
Expand Down Expand Up @@ -151,11 +156,10 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
for (let parent of sorted) {
if (!('nodes' in parent)) continue

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

let candidates = node.params.split(/\s+/g)
let candidates = child.params.split(/\s+/g)

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

parent.nodes.splice(i, 1, ...newNodes)
replaceWith(newNodes)
}
}
})
}

return features
}

Expand Down
78 changes: 78 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,84 @@ describe('@apply', () => {
}"
`)
})

// https://github.com/tailwindlabs/tailwindcss/issues/17924
it('should correctly apply nested usages of @apply when one @utility applies another', async () => {
expect(
await compileCss(
css`
@theme {
--color-green-500: green;
--color-red-500: red;
--color-indigo-500: indigo;
}

@tailwind utilities;

@utility test2 {
@apply test;
}

@utility test {
@apply bg-green-500;
&:hover {
@apply bg-red-500;
}
&:disabled {
@apply bg-indigo-500;
}
}

.foo {
@apply test2;
}
`,
['foo', 'test', 'test2'],
),
).toMatchInlineSnapshot(`
":root, :host {
--color-green-500: green;
--color-red-500: red;
--color-indigo-500: indigo;
}

.test {
background-color: var(--color-green-500);
}

.test:hover {
background-color: var(--color-red-500);
}

.test:disabled {
background-color: var(--color-indigo-500);
}

.test2 {
background-color: var(--color-green-500);
}

.test2:hover {
background-color: var(--color-red-500);
}

.test2:disabled {
background-color: var(--color-indigo-500);
}

.foo {
background-color: var(--color-green-500);
}

.foo:hover {
background-color: var(--color-red-500);
}

.foo:disabled {
background-color: var(--color-indigo-500);
}"
`)
})
})

describe('arbitrary variants', () => {
Expand Down