Ensure --value(…) is required in functional @utility definitions (#20005)

While working on #19989, I noticed that `--value(…)` inside functional
`@utility` definitions is not required right now.

That means that the following CSS is valid:
```css
@utility foo-* {
  color: red;
}
```

But this doesn't really makes sense, because this now accepts a value
and `foo-a`, `foo-b` and `foo-c` would generate the following CSS:
```css
.foo-a {
  color: red;
}
.foo-b {
  color: red;
}
.foo-c {
  color: red;
}
```
The `a`, `b`, and `c` are not doing anything here apart from making your
CSS bigger. So this is very likely an actual bug that you forgot to use
`--value(…)`.

Additionally, if a `--value(…)` was used, but it didn't resolve
anything, then we already properly discared the candidate.

## Test plan

1. Add test to ensure `--value(…)` is required in functional `@utility`
definitions
2. Existing tests pass
This commit is contained in:
Robin Malfait 2026-05-04 16:44:28 +02:00 • committed by GitHub
parent 6e2b60e84a
commit 0f6f7d480f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 44 additions and 8 deletions

View file

@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Allow multiple `@utility` definitions with the same name but different value types ([#19777](https://github.com/tailwindlabs/tailwindcss/pull/19777))
- Export missing `PluginWithConfig` type from `tailwindcss/plugin` to fix errors when inferring plugin config types ([#19707](https://github.com/tailwindlabs/tailwindcss/pull/19707))
- Ensure `start` and `end` legacy utilities without values do not generate CSS ([#20003](https://github.com/tailwindlabs/tailwindcss/pull/20003))
- Ensure `--value(…)` is required in functional `@utility` definitions ([#20005](https://github.com/tailwindlabs/tailwindcss/pull/20005))
## [4.2.4] - 2026-04-21

View file

@ -28870,6 +28870,40 @@ describe('custom utilities', () => {
})
describe('functional utilities', () => {
test('functional utilities require a `--value(…)`', async () => {
let input = css`
@utility tab-* {
tab-size: 4;
}
@tailwind utilities;
`
expect(await compileCss(input, ['tab', 'tab-foo'])).toEqual('')
})
test('functional utilities must resolve at least one `--value(…)`', async () => {
let input = css`
@utility tab-* {
tab-size: --value(integer);
}
@tailwind utilities;
`
expect(await compileCss(input, ['tab-1', 'tab-2'])).toMatchInlineSnapshot(`
".tab-1 {
tab-size: 1;
}
.tab-2 {
tab-size: 2;
}"
`)
expect(await compileCss(input, ['tab', 'tab-foo', 'tab-2.5'])).toEqual('')
})
test('resolving values from `@theme`', async () => {
let input = css`
@theme reference {

View file

@ -6178,14 +6178,14 @@ export function createCssUtility(node: AtRule) {
let shouldRemoveDeclaration = false
let valueAst = ValueParser.parse(node.value)
walk(valueAst, (valueNode) => {
if (valueNode.kind !== 'function') return
walk(valueAst, (fnNode) => {
if (fnNode.kind !== 'function') return
// Value function, e.g.: `--value(integer)`
if (valueNode.value === '--value') {
if (fnNode.value === '--value') {
usedValueFn = true
let resolved = resolveValueFunction(value, valueNode, designSystem)
let resolved = resolveValueFunction(value, fnNode, designSystem)
if (resolved) {
resolvedValueFn = true
if (resolved.ratio) {
@ -6203,7 +6203,7 @@ export function createCssUtility(node: AtRule) {
}
// Modifier function, e.g.: `--modifier(integer)`
else if (valueNode.value === '--modifier') {
else if (fnNode.value === '--modifier') {
// If there is no modifier present in the candidate, then the
// declaration can be removed.
if (modifier === null) {
@ -6213,7 +6213,7 @@ export function createCssUtility(node: AtRule) {
usedModifierFn = true
let replacement = resolveValueFunction(modifier, valueNode, designSystem)
let replacement = resolveValueFunction(modifier, fnNode, designSystem)
if (replacement) {
resolvedModifierFn = true
return WalkAction.ReplaceSkip(replacement.nodes)
@ -6233,8 +6233,9 @@ export function createCssUtility(node: AtRule) {
node.value = ValueParser.toCss(valueAst)
})
// Used `--value(…)` but nothing resolved
if (usedValueFn && !resolvedValueFn) return null
// Functional CSS utilities require `--value(…)`, and one of those
// branches must resolve for the candidate to be valid.
if (!usedValueFn || !resolvedValueFn) return null
// Used `--modifier(…)` but nothing resolved
if (usedModifierFn && !resolvedModifierFn) return null