Skip to content

Commit f036f80

Browse files
mvanhornclaudeRobinMalfait
authored
Allow multiple @Utility definitions with same name but different value types (#19777)
## Summary Fixes #16948 When defining multiple CSS `@utility foo-*` with different value types (e.g., one for colors, one for numbers), only the first handler was tried. If it returned `null` (value didn't match), the compile loop stopped, preventing subsequent handlers from being attempted. ```css @Utility foo-* { color: --value(--color-*); } @Utility foo-* { font-size: --spacing(--value(number)); } ``` Previously, `foo-red-500` worked but `foo-123` did not (or vice versa depending on definition order). The fix distinguishes between CSS `@utility` handlers and JS plugin `matchUtilities` handlers: - **CSS `@utility`** (no typed options): `null` means "try the next handler" - allows multiple definitions with different value types to coexist - **JS `matchUtilities`** (with explicit types): `null` means "the value was invalid for this type, stop" - preserves existing behavior where typed utilities prevent invalid values from falling through ## Test plan - Added test: two `@utility foo-*` definitions with different value types - verifies both `foo-red-500` (color) and `foo-123` (number) produce correct CSS - All 4621 existing tests pass (including the `matchUtilities` type-safety tests) - `pnpm build && pnpm test` passes This contribution was developed with AI assistance (Claude Code). --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
1 parent d194d4c commit f036f80

3 files changed

Lines changed: 61 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
- 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))
2323
- Canonicalization: migrate arbitrary `:has()` variants from `[&:has(…)]` to `has-[…]` ([#19991](https://github.com/tailwindlabs/tailwindcss/pull/19991))
2424
- Upgrade: don’t migrate inline `style` attributes ([#19918](https://github.com/tailwindlabs/tailwindcss/pull/19918))
25+
- Allow multiple `@utility` definitions with the same name but different value types ([#19777](https://github.com/tailwindlabs/tailwindcss/pull/19777))
2526

2627
## [4.2.4] - 2026-04-21
2728

packages/tailwindcss/src/compile.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,19 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) {
287287

288288
let compiledNodes = utility.compileFn(candidate)
289289
if (compiledNodes === undefined) continue
290-
if (compiledNodes === null) return asts
290+
if (compiledNodes === null) {
291+
// `null` means that the result is invalid and that this plugin should not
292+
// result in any CSS, but that doesn't mean that subsequent plugins are
293+
// invalid as well.
294+
//
295+
// However, for backwards compatibility with `matchUtilities` this means
296+
// that we do need to bail entirely: plugins that handle a specific
297+
// arbitrary value type prevent falling through to other plugins if the
298+
// result is invalid for that plugin
299+
if (utility.options?.types?.length) return asts
300+
301+
continue
302+
}
291303
asts.push(compiledNodes)
292304
}
293305

@@ -299,7 +311,19 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) {
299311

300312
let compiledNodes = utility.compileFn(candidate)
301313
if (compiledNodes === undefined) continue
302-
if (compiledNodes === null) return asts
314+
if (compiledNodes === null) {
315+
// `null` means that the result is invalid and that this plugin should not
316+
// result in any CSS, but that doesn't mean that subsequent plugins are
317+
// invalid as well.
318+
//
319+
// However, for backwards compatibility with `matchUtilities` this means
320+
// that we do need to bail entirely: plugins that handle a specific
321+
// arbitrary value type prevent falling through to other plugins if the
322+
// result is invalid for that plugin
323+
if (utility.options?.types?.length) return asts
324+
325+
continue
326+
}
303327
asts.push(compiledNodes)
304328
}
305329

packages/tailwindcss/src/utilities.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29979,4 +29979,38 @@ describe('custom utilities', () => {
2997929979
}"
2998029980
`)
2998129981
})
29982+
29983+
test('multiple @utility definitions with the same name but different value types', async () => {
29984+
let input = css`
29985+
@theme {
29986+
--color-red-500: #ef4444;
29987+
--spacing: 0.25rem;
29988+
}
29989+
29990+
@utility foo-* {
29991+
color: --value(--color-*);
29992+
}
29993+
29994+
@utility foo-* {
29995+
font-size: --spacing(--value(number));
29996+
}
29997+
29998+
@tailwind utilities;
29999+
`
30000+
30001+
expect(await compileCss(input, ['foo-red-500', 'foo-123'])).toMatchInlineSnapshot(`
30002+
":root, :host {
30003+
--color-red-500: #ef4444;
30004+
--spacing: .25rem;
30005+
}
30006+
30007+
.foo-123 {
30008+
font-size: calc(var(--spacing) * 123);
30009+
}
30010+
30011+
.foo-red-500 {
30012+
color: var(--color-red-500);
30013+
}"
30014+
`)
30015+
})
2998230016
})

0 commit comments

Comments
 (0)