--default(…) should emit values as-is (#20392)

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
<div className="trim-both" />
```

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.
This commit is contained in:
Robin Malfait 2026-08-06 15:25:48 +02:00 • committed by GitHub
parent b8a5f85889
commit e566a926e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 38 additions and 0 deletions

View file

@ -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<flex>`, `%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

View file

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

View file

@ -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, '*')