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