Skip to content

Commit 08cad84

Browse files
authored
Support --default(…) in --value(…) and --modifier(…) to support fallback values (#19989)
This PR adds a new `--default(…)` option that can be used inside `--value(…)` or `--modifier(…)` such that functional utilities without an explicit value/modifier can still be defined as a functional utility. It would also allow you to use a functional utility without a value and _with_ a modifier, e.g.: `shadow/50`. --- This allows us to re-implement functional utilities with a default value in CSS using `@utility`. Used the explicit `--default(…)` argument of `--value(…)` for a few reasons. 1. It's explicit about being a falllback value. If you have `@utility foo-*`, then you want to be able to use `foo`, but `foo-bad` should not compile. 2. When `--value(…)` is used in (complex) property values (think a bunch of `calc(…)` expressions), then we don't need a separate property for this. One of the ideas was to have a literal fallback: ```css @Utility tab-* { tab-size: 4; tab-size: --value(number); } ``` For `tab`, this would compile to: ```css .tab { tab-size: 4; } ``` For `tab-123`, this would compile to: ```css .tab { tab-size: 4; tab-size: 123; } ``` Getting rid of the `tab-size: 4` would be an option, but it's a common pattern in real CSS for fallback values (think hex background color, over a more modern `oklch` color). For `tab-foo`, this would compile to: ```css .tab { tab-size: 4; } ``` Which means that we have an infinite amount classes that would result in the same class, which is bad. We could special case this one because the internal `value` would still be `null`, but it might be too confusing. This syntax without the `--default(…)` also means repetition of certain properties. Add `--modifier(…)` to the mix, and there is even more repetition going on. Another option to consider is that the default fallback is just another option in the `--value(…, 4)`, but if a default fallback is a keyword, then there is a chance that this might conflict with actual keywords we interpret. Main motivation is to be able to re-implement utilities such as `shadow/50` purely in CSS. It's also something we support in the JS based APIs, but not in the CSS based one, so while it's a "new" feature, it's more like a missing feature right now, and often a reason for people to use the JS based APIs instead. For consistency reasons, this is also implemented for `--modifier(…)` such that you can use a default value there. E.g. when re-implementing `text-sm` where a default `line-height` is set without the explicit use of a modifier. Fixes: #16824 ## Test plan 1. Added a handful of new tests to make sure this functionality works 2. Existing tests still pass
1 parent 0f6f7d4 commit 08cad84

3 files changed

Lines changed: 151 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- _Experimental_: add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
1313
- Allow using `@variant` with stacked variants (e.g. `@variant hover:focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996))
1414
- Allow using `@variant` with compound variants (e.g. `@variant hover, focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996))
15+
- Support `--default(…)` in `--value(…)` and `--modifier(…)` for functional `@utility` definitions ([#19989](https://github.com/tailwindlabs/tailwindcss/pull/19989))
1516

1617
### Fixed
1718

packages/tailwindcss/src/utilities.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29574,6 +29574,134 @@ describe('custom utilities', () => {
2957429574
`)
2957529575
})
2957629576

29577+
test('functional utilities can use `--default(…)` in `--value(…)`', async () => {
29578+
let input = css`
29579+
@utility tab-* {
29580+
tab-size: --value(integer, --default(4));
29581+
}
29582+
29583+
@tailwind utilities;
29584+
`
29585+
29586+
expect(await compileCss(input, ['tab', 'tab-123'])).toMatchInlineSnapshot(`
29587+
".tab {
29588+
tab-size: 4;
29589+
}
29590+
29591+
.tab-123 {
29592+
tab-size: 123;
29593+
}"
29594+
`)
29595+
29596+
expect(await compileCss(input, ['tab-foo'])).toEqual('')
29597+
})
29598+
29599+
test('functional utilities can use `--default(…)` in complex expressions', async () => {
29600+
let input = css`
29601+
@utility tab-* {
29602+
tab-size: calc(--value(integer, --default(4)) * 2);
29603+
}
29604+
29605+
@tailwind utilities;
29606+
`
29607+
29608+
expect(await compileCss(input, ['tab', 'tab-123'])).toMatchInlineSnapshot(`
29609+
".tab {
29610+
tab-size: 8;
29611+
}
29612+
29613+
.tab-123 {
29614+
tab-size: 246;
29615+
}"
29616+
`)
29617+
29618+
expect(await compileCss(input, ['tab-foo'])).toEqual('')
29619+
})
29620+
29621+
test('functional utilities can use `--default(…)` with `--modifier(…)`', async () => {
29622+
let input = css`
29623+
@utility tab-* {
29624+
tab-size: --value(integer, --default(4));
29625+
line-height: --modifier(integer);
29626+
}
29627+
29628+
@tailwind utilities;
29629+
`
29630+
29631+
expect(await compileCss(input, ['tab', 'tab/25'])).toMatchInlineSnapshot(`
29632+
".tab\\/25 {
29633+
tab-size: 4;
29634+
line-height: 25;
29635+
}
29636+
29637+
.tab {
29638+
tab-size: 4;
29639+
}"
29640+
`)
29641+
29642+
expect(await compileCss(input, ['tab/foo'])).toEqual('')
29643+
})
29644+
29645+
test('functional utilities can use `--default(…)` in `--modifier(…)`', async () => {
29646+
let input = css`
29647+
@utility tab-* {
29648+
tab-size: --value(integer);
29649+
line-height: --modifier(integer, --default(1));
29650+
}
29651+
29652+
@tailwind utilities;
29653+
`
29654+
29655+
expect(await compileCss(input, ['tab-123', 'tab-123/25'])).toMatchInlineSnapshot(`
29656+
".tab-123 {
29657+
tab-size: 123;
29658+
line-height: 1;
29659+
}
29660+
29661+
.tab-123\\/25 {
29662+
tab-size: 123;
29663+
line-height: 25;
29664+
}"
29665+
`)
29666+
29667+
expect(await compileCss(input, ['tab-123/foo'])).toEqual('')
29668+
})
29669+
29670+
test('functional utilities can use `--default(…)` in `--value(…)` and `--modifier(…)`', async () => {
29671+
let input = css`
29672+
@utility tab-* {
29673+
tab-size: --value(integer, --default(12));
29674+
line-height: --modifier(integer, --default(34));
29675+
}
29676+
29677+
@tailwind utilities;
29678+
`
29679+
29680+
expect(await compileCss(input, ['tab', 'tab/1', 'tab-1', 'tab-1/1'])).toMatchInlineSnapshot(`
29681+
".tab {
29682+
tab-size: 12;
29683+
line-height: 34;
29684+
}
29685+
29686+
.tab-1 {
29687+
tab-size: 1;
29688+
line-height: 34;
29689+
}
29690+
29691+
.tab-1\\/1 {
29692+
tab-size: 1;
29693+
line-height: 1;
29694+
}
29695+
29696+
.tab\\/1 {
29697+
tab-size: 12;
29698+
line-height: 1;
29699+
}"
29700+
`)
29701+
29702+
expect(await compileCss(input, ['tab-123/foo'])).toEqual('')
29703+
})
29704+
2957729705
test('modifiers', async () => {
2957829706
let input = css`
2957929707
@theme reference {

packages/tailwindcss/src/utilities.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5956,6 +5956,8 @@ export function createCssUtility(node: AtRule) {
59565956
// - `--value(number)` resolves a bare value of type number
59575957
// - `--value([number])` resolves an arbitrary value of type number
59585958
// - `--value(--color)` resolves a theme value in the `color` namespace
5959+
// - `--value(--default(4))` resolves to a default value when only the
5960+
// root of the functional utility was used.
59595961
// - `--value(number, [number])` resolves a bare value of type number or an
59605962
// arbitrary value of type number in order.
59615963
//
@@ -6069,7 +6071,7 @@ export function createCssUtility(node: AtRule) {
60696071
arg = arg.replace(/(-\*){2,}/g, '-*')
60706072

60716073
// Ensure trailing `-*` exists if `-*` isn't present yet
6072-
if (arg[0] === '-' && arg[1] === '-' && !arg.includes('-*')) {
6074+
if (arg[0] === '-' && arg[1] === '-' && !arg.includes('(') && !arg.includes('-*')) {
60736075
arg += '-*'
60746076
}
60756077

@@ -6135,9 +6137,8 @@ export function createCssUtility(node: AtRule) {
61356137
let value = candidate.value
61366138
let modifier = candidate.modifier
61376139

6138-
// A value is required for functional utilities, if you want to accept
6139-
// just `tab-size`, you'd have to use a static utility.
6140-
if (value === null) return
6140+
// Functional CSS utilities must resolve at least one `--value(…)`.
6141+
// Use `--default(…)` inside `--value(…)` for the omitted-value case.
61416142

61426143
// Whether `--value(…)` was used
61436144
let usedValueFn = false
@@ -6197,30 +6198,21 @@ export function createCssUtility(node: AtRule) {
61976198
}
61986199

61996200
// Drop the declaration in case we couldn't resolve the value
6200-
usedValueFn ||= false
62016201
shouldRemoveDeclaration = true
62026202
return WalkAction.Stop
62036203
}
62046204

62056205
// Modifier function, e.g.: `--modifier(integer)`
62066206
else if (fnNode.value === '--modifier') {
6207-
// If there is no modifier present in the candidate, then the
6208-
// declaration can be removed.
6209-
if (modifier === null) {
6210-
shouldRemoveDeclaration = true
6211-
return WalkAction.Stop
6212-
}
6213-
62146207
usedModifierFn = true
62156208

6216-
let replacement = resolveValueFunction(modifier, fnNode, designSystem)
6217-
if (replacement) {
6209+
let resolved = resolveValueFunction(modifier, fnNode, designSystem)
6210+
if (resolved) {
62186211
resolvedModifierFn = true
6219-
return WalkAction.ReplaceSkip(replacement.nodes)
6212+
return WalkAction.ReplaceSkip(resolved.nodes)
62206213
}
62216214

62226215
// Drop the declaration in case we couldn't resolve the value
6223-
usedModifierFn ||= false
62246216
shouldRemoveDeclaration = true
62256217
return WalkAction.Stop
62266218
}
@@ -6238,7 +6230,7 @@ export function createCssUtility(node: AtRule) {
62386230
if (!usedValueFn || !resolvedValueFn) return null
62396231

62406232
// Used `--modifier(…)` but nothing resolved
6241-
if (usedModifierFn && !resolvedModifierFn) return null
6233+
if (usedModifierFn && !resolvedModifierFn && modifier !== null) return null
62426234

62436235
// Resolved `--value(ratio)` and `--modifier(…)`, which is invalid
62446236
if (resolvedRatioValue && resolvedModifierFn) return null
@@ -6309,13 +6301,23 @@ export function createCssUtility(node: AtRule) {
63096301
}
63106302

63116303
function resolveValueFunction(
6312-
value: NonNullable<
6304+
value:
63136305
| Extract<Candidate, { kind: 'functional' }>['value']
6314-
| Extract<Candidate, { kind: 'functional' }>['modifier']
6315-
>,
6306+
| Extract<Candidate, { kind: 'functional' }>['modifier'],
63166307
fn: ValueParser.ValueFunctionNode,
63176308
designSystem: DesignSystem,
63186309
): { nodes: ValueParser.ValueAstNode[]; ratio?: boolean } | undefined {
6310+
// No value provided, we can try `--default(…)`
6311+
if (value === null) {
6312+
for (let arg of fn.nodes) {
6313+
// Resolve default value, e.g.: `--default(…)`
6314+
if (arg.kind === 'function' && arg.value === '--default') {
6315+
return { nodes: arg.nodes }
6316+
}
6317+
}
6318+
return
6319+
}
6320+
63196321
for (let arg of fn.nodes) {
63206322
// Resolve literal value, e.g.: `--modifier('closest-side')`
63216323
if (

0 commit comments

Comments
 (0)