From e566a926e774b2060f1e6ac19fc4374126f83108 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 6 Aug 2026 15:25:48 +0200 Subject: [PATCH] =?UTF-8?q?`--default(=E2=80=A6)`=20should=20emit=20values?= =?UTF-8?q?=20as-is=20(#20392)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes an issue where the `--default(…)` function that can be used as part of `@utility` definitions goes through the same pre-processing treatment as `--value(…)` and `--modifier(…)`. Instead the `--default(…)` value should be emitted as-is. The pre-processing treatment that's in place today is just to normalize the value(s) that could be formatted in a strange way if you are dealing with older Prettier / Biome versions. Some of the transformations we do ahead of time: - `--value(--spacing)` -> `--value(--spacing-*)` - `--value(--spacing- *)` -> `--value(--spacing-*)` - `--value(--text- * --line-height)` -> `--value(--text-*--line-height)` - `--value(--text --line-height)` -> `--value(--text-*--line-height)` - `--value(--text-\\* --line-height)` -> `--value(--text-*--line-height)` - `--value([ *])` -> `--value([*])` One of these steps is getting rid of the spaces, that means that if your `--default(…)` uses spaces, then we will get rid of it. ```css @theme inline { --text-box-trim-start: trim-start; --text-box-trim-end: trim-end; --text-box-trim-both: trim-both; --text-box-edge-cap: cap alphabetic; --text-box-edge-cap-text: cap text; --text-box-edge-ex: ex alphabetic; --text-box-edge-ex-text: ex text; --text-box-edge-text: text alphabetic; --text-box-edge-text-text: text text; } @utility trim-* { text-box: --value('normal', 'inherit', 'initial', 'revert', 'revert-layer', 'unset'); text-box: --value(--text-box-trim-*) --modifier(--text-box-edge-*, --default(box alphabetic)); } ``` When you try to use this: ```html
``` Then we get the following CSS out: ```css .trim-both { text-box: trim-both boxalphabetic; } ``` This PR fixes that, and now the proper CSS will be emitted: ```css .trim-both { text-box: trim-both box alphabetic; } ``` Fixes: #20391 ## Test plan 1. Added an regression test to make sure that `--default(…)` values are emitted as-is. --- CHANGELOG.md | 1 + packages/tailwindcss/src/utilities.test.ts | 28 ++++++++++++++++++++++ packages/tailwindcss/src/utilities.ts | 9 +++++++ 3 files changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba736074e..64c0ea940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prevent `@tailwindcss/vite` from crashing on every edit under Vite's experimental `bundledDev` mode ([#20379](https://github.com/tailwindlabs/tailwindcss/pull/20379)) - Ensure `@tailwindcss/oxide` falls back to WASM on platforms without native bindings ([#20383](https://github.com/tailwindlabs/tailwindcss/pull/20383)) - Detect classes in Ruby percent literals using angle brackets or custom delimiters (e.g. `%w`, `%w|flex|`), including in Slim and Haml templates ([#20387](https://github.com/tailwindlabs/tailwindcss/pull/20387)) +- Preserve whitespace in `--default(…)` values in custom functional utilities (e.g. `--default(box alphabetic)` no longer becomes `boxalphabetic`) ([#20392](https://github.com/tailwindlabs/tailwindcss/pull/20392)) ## [4.3.3] - 2026-07-16 diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 9b514cda7..c9d4a5ff0 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -31000,6 +31000,34 @@ describe('custom utilities', () => { expect(await run(['example-123/foo'], input)).toEqual('') }) + test('the value of `--default(…)` is emitted as-is', async () => { + let input = css` + @theme reference { + --text-box-trim-trim-both: trim-both; + --text-box-edge-cap: cap alphabetic; + } + + @utility trim-* { + text-box: --value(--text-box-trim-*) + --modifier(--text-box-edge-*, --default(box alphabetic)); + } + + @tailwind utilities; + ` + + expect(await run(['trim-trim-both', 'trim-trim-both/cap'], input)).toMatchInlineSnapshot(` + " + .trim-trim-both { + text-box: var(--text-box-trim-trim-both, trim-both) box alphabetic; + } + + .trim-trim-both\\/cap { + text-box: var(--text-box-trim-trim-both, trim-both) var(--text-box-edge-cap, cap alphabetic); + } + " + `) + }) + test('modifiers', async () => { let input = css` @theme reference { diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 54b472c60..6c4a20cbc 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -6193,6 +6193,15 @@ export function createCssUtility(node: AtRule) { let args = segment(ValueParser.toCss(fn.nodes), ',') for (let [idx, arg] of args.entries()) { + arg = arg.trim() + + // The value of `--default(…)` is emitted as-is, so it should not be + // normalized. + if (arg.startsWith('--default(')) { + args[idx] = arg + continue + } + // Transform escaped `\\*` -> `*` arg = arg.replace(/\\\*/g, '*')