From c8f752f6bd56d4bb8bd31cb678c38c1535022854 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Tue, 10 Mar 2026 01:15:17 -0700 Subject: [PATCH] fix(compile): allow multiple @utility definitions with same name but different value types When multiple CSS @utility definitions share the same name but handle different value types, all handlers should be tried. Previously, a null return from any handler would stop the loop, preventing subsequent handlers from being attempted. The fix distinguishes between CSS @utility handlers (no typed options) and JS plugin matchUtilities handlers (with explicit types). For CSS @utility, null means "try the next handler." For typed plugin utilities, null means "the value was invalid for this type, stop." Fixes #16948 Co-Authored-By: Claude Opus 4.6 --- packages/tailwindcss/src/compile.ts | 14 +++++++-- packages/tailwindcss/src/utilities.test.ts | 34 ++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 82ac77eb1d6e..246bc1f7e2d5 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -287,7 +287,14 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let compiledNodes = utility.compileFn(candidate) if (compiledNodes === undefined) continue - if (compiledNodes === null) return asts + if (compiledNodes === null) { + // For typed plugin utilities (matchUtilities with explicit types), + // null means the value was invalid for this type - stop trying. + // For CSS @utility definitions (no types), null means the value + // didn't match this handler - try the next one. + if (utility.options?.types?.length) return asts + continue + } asts.push(compiledNodes) } @@ -299,7 +306,10 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let compiledNodes = utility.compileFn(candidate) if (compiledNodes === undefined) continue - if (compiledNodes === null) return asts + if (compiledNodes === null) { + if (utility.options?.types?.length) return asts + continue + } asts.push(compiledNodes) } diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index e562cd3be15a..99af6a5dca2a 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -29979,4 +29979,38 @@ describe('custom utilities', () => { }" `) }) + + test('multiple @utility definitions with the same name but different value types', async () => { + let input = css` + @theme { + --color-red-500: #ef4444; + --spacing: 0.25rem; + } + + @utility foo-* { + color: --value(--color-*); + } + + @utility foo-* { + font-size: --spacing(--value(number)); + } + + @tailwind utilities; + ` + + expect(await compileCss(input, ['foo-red-500', 'foo-123'])).toMatchInlineSnapshot(` + ":root, :host { + --color-red-500: #ef4444; + --spacing: .25rem; + } + + .foo-123 { + font-size: calc(var(--spacing) * 123); + } + + .foo-red-500 { + color: var(--color-red-500); + }" + `) + }) })