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>
This commit is contained in:
Matt Van Horn 2026-04-29 07:56:17 -07:00 • committed by GitHub
parent d194d4c3e6
commit f036f80195
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 61 additions and 2 deletions

View file

@ -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

View file

@ -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)
}

View file

@ -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);
}"
`)
})
})