From c59321dcc032c746145a1609a6e453d726cd8ac6 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 10:46:14 -0500 Subject: [PATCH 1/5] Tweak code style --- packages/tailwindcss/src/apply.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index c7bcc5e28801..36f882b673fd 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -166,7 +166,10 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { let offset = 0 for (let [idx, part] of parts.entries()) { - if (idx % 2 === 0) candidateOffsets[part] = offset + if (idx % 2 === 0) { + candidateOffsets[part] = offset + } + offset += part.length } From d975ffa632f59c5d4f26b65c8a83bf4aba74dd08 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 10:46:20 -0500 Subject: [PATCH 2/5] Handle CSS mixins inside `@apply` --- packages/tailwindcss/src/apply.ts | 28 +++++++++++++ packages/tailwindcss/src/ast.ts | 3 +- packages/tailwindcss/src/index.test.ts | 54 ++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 36f882b673fd..60d430731709 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -163,16 +163,44 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { let parts = child.params.split(/(\s+)/g) let candidateOffsets: Record = {} + let normalIdents: string[] = [] + let dashedIdents: string[] = [] let offset = 0 for (let [idx, part] of parts.entries()) { if (idx % 2 === 0) { + if (part[0] === '-' && part[1] === '-') { + dashedIdents.push(part) + } else { + normalIdents.push(part) + } + candidateOffsets[part] = offset } offset += part.length } + if (dashedIdents.length) { + // If we have an `@apply` that only consists of dashed idents then the + // user is intending to use a CSS mixin: + // https://drafts.csswg.org/css-mixins-1/#apply-rule + // + // These are not considered utilities and need to be emitted literally. + if (normalIdents.length === 0) return WalkAction.Skip + + // If we find a dashed ident *here* it means that someone is trying + // to use mixins and our `@apply` behavior together. + // + // This is invalid and the rules must be written separately. Let the + // user know they need to move them into a separate rule. + let list = dashedIdents.join(' ') + + throw new Error( + `You cannot use \`@apply\` with both mixins and utilities. Please move \`@apply ${list}\` into a separate rule.`, + ) + } + // Replace the `@apply` rule with the actual utility classes { // Parse the candidates to an AST that we can replace the `@apply` rule diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index ab0a4fb4d572..04a954c1023c 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -400,7 +400,8 @@ export function optimizeAst( copy.name === '@charset' || copy.name === '@custom-media' || copy.name === '@namespace' || - copy.name === '@import' + copy.name === '@import' || + copy.name === '@apply' ) { parent.push(copy) } diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 5cd5282b00f7..267a3c512f80 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -855,6 +855,60 @@ describe('@apply', () => { }" `) }) + + it('should be usable with CSS mixins', async () => { + let compiler = await compile(css` + .foo { + /* Utility usage */ + @apply underline; + + /* CSS mixin usage */ + @apply --my-mixin-1; + @apply --my-mixin-1(); + @apply --my-mixin-1 --my-mixin-2; + @apply --my-mixin-1() --my-mixin-2(); + @apply --my-mixin-3 { + color: red; + } + } + `) + + expect(compiler.build([])).toMatchInlineSnapshot(` + ".foo { + text-decoration-line: underline; + @apply --my-mixin-1; + @apply --my-mixin-1(); + @apply --my-mixin-1 --my-mixin-2; + @apply --my-mixin-1() --my-mixin-2(); + @apply --my-mixin-3 { + color: red; + } + } + " + `) + }) + + it('should error when trying to use mixins and utilities together', async () => { + await expect( + compile(css` + .foo { + @apply underline --my-mixin-1; + } + `), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: You cannot use \`@apply\` with both mixins and utilities. Please move \`@apply --my-mixin-1\` into a separate rule.]`, + ) + + await expect( + compile(css` + .foo { + @apply --my-mixin-1 underline; + } + `), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: You cannot use \`@apply\` with both mixins and utilities. Please move \`@apply --my-mixin-1\` into a separate rule.]`, + ) + }) }) describe('arbitrary variants', () => { From 7d79b2a02ef1cf8d43f6dd0f470800e827656a90 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 10:29:11 -0500 Subject: [PATCH 3/5] =?UTF-8?q?Don=E2=80=99t=20allow=20utility-based=20`@a?= =?UTF-8?q?pply`=20to=20be=20used=20with=20a=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/tailwindcss/src/apply.ts | 13 +++++++++++++ packages/tailwindcss/src/index.test.ts | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 60d430731709..19a2de5f96f7 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -201,6 +201,19 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { ) } + // If we find a dashed ident *here* it means that someone is trying + // to use mixins and our `@apply` behavior together. + // + // This is considered invalid usage and we want to inform the user + // as such. + let hasBody = child.nodes.length > 0 + + if (hasBody && normalIdents.length) { + let list = normalIdents.join(' ') + + throw new Error(`The rule \`@apply ${list}\` must not have a body.`) + } + // Replace the `@apply` rule with the actual utility classes { // Parse the candidates to an AST that we can replace the `@apply` rule diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 267a3c512f80..e1697800fdde 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -909,6 +909,20 @@ describe('@apply', () => { `[Error: You cannot use \`@apply\` with both mixins and utilities. Please move \`@apply --my-mixin-1\` into a separate rule.]`, ) }) + + it('should error when used with a body', async () => { + await expect( + compile(css` + .foo { + @apply underline { + color: red; + } + } + `), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: The rule \`@apply underline\` must not have a body.]`, + ) + }) }) describe('arbitrary variants', () => { From 19a08ed15de71ba9c3a09766f051357ba7d2843a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 10:59:42 -0500 Subject: [PATCH 4/5] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc2c8629be8..881d9d04851f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Correctly handle duplicate CLI arguments ([#19416](https://github.com/tailwindlabs/tailwindcss/pull/19416)) - Don’t emit color-mix fallback rules inside `@keyframes` ([#19419](https://github.com/tailwindlabs/tailwindcss/pull/19419)) - CLI: Don't hang when output is `/dev/stdout` ([#19421](https://github.com/tailwindlabs/tailwindcss/pull/19421)) +- Allow `@apply` to be used with CSS mixins ([#19427](https://github.com/tailwindlabs/tailwindcss/pull/19427)) ### Added From 095370ff747929103cb71a3a54160157b8531a97 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 11:17:09 -0500 Subject: [PATCH 5/5] drop comment --- packages/tailwindcss/src/apply.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 19a2de5f96f7..2d5e5b847bbc 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -201,11 +201,6 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { ) } - // If we find a dashed ident *here* it means that someone is trying - // to use mixins and our `@apply` behavior together. - // - // This is considered invalid usage and we want to inform the user - // as such. let hasBody = child.nodes.length > 0 if (hasBody && normalIdents.length) {