From 0f6f7d480f8217b7eb17daaec28d3af07009c15f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 4 May 2026 16:44:28 +0200 Subject: [PATCH] =?UTF-8?q?Ensure=20`--value(=E2=80=A6)`=20is=20required?= =?UTF-8?q?=20in=20functional=20`@utility`=20definitions=20(#20005)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 1 + packages/tailwindcss/src/utilities.test.ts | 34 ++++++++++++++++++++++ packages/tailwindcss/src/utilities.ts | 17 ++++++----- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6991c744c..971ad9629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 5ce6ad284..37c5945f6 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -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 { diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index a18def9e1..2aa25fc25 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -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