diff --git a/CHANGELOG.md b/CHANGELOG.md index b138c05a4..540903b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Canonicalization: preserve the original unit in arbitrary values instead of normalizing to base units (e.g. `-mt-[20in]` → `mt-[-20in]`, not `mt-[-1920px]`) ([#19988](https://github.com/tailwindlabs/tailwindcss/pull/19988)) - Canonicalization: migrate arbitrary `:has()` variants from `[&:has(…)]` to `has-[…]` ([#19991](https://github.com/tailwindlabs/tailwindcss/pull/19991)) - Upgrade: don’t migrate inline `style` attributes ([#19918](https://github.com/tailwindlabs/tailwindcss/pull/19918)) +- Allow multiple `@utility` definitions with the same name but different value types ([#19777](https://github.com/tailwindlabs/tailwindcss/pull/19777)) ## [4.2.4] - 2026-04-21 diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 82ac77eb1..9bfb3fb18 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -287,7 +287,19 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let compiledNodes = utility.compileFn(candidate) if (compiledNodes === undefined) continue - if (compiledNodes === null) return asts + if (compiledNodes === null) { + // `null` means that the result is invalid and that this plugin should not + // result in any CSS, but that doesn't mean that subsequent plugins are + // invalid as well. + // + // However, for backwards compatibility with `matchUtilities` this means + // that we do need to bail entirely: plugins that handle a specific + // arbitrary value type prevent falling through to other plugins if the + // result is invalid for that plugin + if (utility.options?.types?.length) return asts + + continue + } asts.push(compiledNodes) } @@ -299,7 +311,19 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let compiledNodes = utility.compileFn(candidate) if (compiledNodes === undefined) continue - if (compiledNodes === null) return asts + if (compiledNodes === null) { + // `null` means that the result is invalid and that this plugin should not + // result in any CSS, but that doesn't mean that subsequent plugins are + // invalid as well. + // + // However, for backwards compatibility with `matchUtilities` this means + // that we do need to bail entirely: plugins that handle a specific + // arbitrary value type prevent falling through to other plugins if the + // result is invalid for that plugin + 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 4e4f1feee..e57fe9cd6 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); + }" + `) + }) })