Add support for literal values in --value('…') and --modifier('…') (#17304)

This PR adds support for literal values inside the `--value('…')` and
`--modifier('…')` functions. This allows you to safelist some known
values you want to use:

E.g.:

```css
@utility tab-* {
  tab-size: --value('revert', 'initial');
}
```

This allows you to use `tab-revert` and `tab-initial` for example.
This commit is contained in:
Robin Malfait 2025-03-20 20:02:02 +01:00 committed by GitHub
parent 4c57d9f734
commit a3316f2ef4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 101 additions and 23 deletions

View File

@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Experimental_: Add `user-valid` and `user-invalid` variants ([#12370](https://github.com/tailwindlabs/tailwindcss/pull/12370))
- _Experimental_: Add `wrap-anywhere`, `wrap-break-word`, and `wrap-normal` utilities ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128))
- _Experimental_: Add `@source inline(…)` ([#17147](https://github.com/tailwindlabs/tailwindcss/pull/17147))
- Add support for literal values in `--value('…')` and `--modifier('…')` ([#17304](https://github.com/tailwindlabs/tailwindcss/pull/17304))
### [4.0.15] - 2025-03-20

View File

@ -479,13 +479,13 @@ test('Custom functional @utility', async () => {
}
@utility tab-* {
tab-size: --value(--tab-size);
tab-size: --value(--tab-size, 'revert', 'initial');
}
@utility example-* {
font-size: --value(--text);
line-height: --value(--text- * --line-height);
line-height: --modifier(--leading);
line-height: --modifier(--leading, 'normal');
}
@utility -negative-* {
@ -507,6 +507,8 @@ test('Custom functional @utility', async () => {
expect(classNames).toContain('tab-2')
expect(classNames).toContain('tab-4')
expect(classNames).toContain('tab-github')
expect(classNames).toContain('tab-revert')
expect(classNames).toContain('tab-initial')
expect(classNames).not.toContain('-tab-1')
expect(classNames).not.toContain('-tab-2')
@ -524,7 +526,7 @@ test('Custom functional @utility', async () => {
expect(classNames).not.toContain('--negative-github')
expect(classNames).toContain('example-xs')
expect(classMap.get('example-xs')?.modifiers).toEqual(['foo', 'bar'])
expect(classMap.get('example-xs')?.modifiers).toEqual(['normal', 'foo', 'bar'])
})
test('Theme keys with underscores are suggested with underscores', async () => {

View File

@ -17256,6 +17256,23 @@ describe('custom utilities', () => {
expect(await compileCss(input, ['tab-foo'])).toEqual('')
})
test('resolve literal values', async () => {
let input = css`
@utility tab-* {
tab-size: --value('revert');
}
@tailwind utilities;
`
expect(await compileCss(input, ['tab-revert'])).toMatchInlineSnapshot(`
".tab-revert {
tab-size: revert;
}"
`)
expect(await compileCss(input, ['tab-initial'])).toEqual('')
})
test('resolving bare values with constraints for integer, percentage, and ratio', async () => {
let input = css`
@utility example-* {
@ -17720,6 +17737,7 @@ describe('custom utilities', () => {
--value: --value(--value, [length]);
--modifier: --modifier(--modifier, [length]);
--modifier-with-calc: calc(--modifier(--modifier, [length]) * 2);
--modifier-literals: --modifier('literal', 'literal-2');
}
@tailwind utilities;
@ -17731,6 +17749,8 @@ describe('custom utilities', () => {
'example-sm/7',
'example-[12px]',
'example-[12px]/[16px]',
'example-sm/literal',
'example-sm/literal-2',
]),
).toMatchInlineSnapshot(`
".example-\\[12px\\]\\/\\[16px\\] {
@ -17745,6 +17765,16 @@ describe('custom utilities', () => {
--modifier-with-calc: calc(var(--modifier-7, 28px) * 2);
}
.example-sm\\/literal {
--value: var(--value-sm, 14px);
--modifier-literals: literal;
}
.example-sm\\/literal-2 {
--value: var(--value-sm, 14px);
--modifier-literals: literal-2;
}
.example-\\[12px\\] {
--value: 12px;
}
@ -17754,7 +17784,12 @@ describe('custom utilities', () => {
}"
`)
expect(
await compileCss(input, ['example-foo', 'example-foo/[12px]', 'example-foo/12']),
await compileCss(input, [
'example-foo',
'example-foo/[12px]',
'example-foo/12',
'example-sm/unknown-literal',
]),
).toEqual('')
})

View File

@ -4706,6 +4706,7 @@ export function createCssUtility(node: AtRule) {
if (IS_VALID_FUNCTIONAL_UTILITY_NAME.test(name)) {
// API:
//
// - `--value('literal')` resolves a literal named value
// - `--value(number)` resolves a bare value of type number
// - `--value([number])` resolves an arbitrary value of type number
// - `--value(--color)` resolves a theme value in the `color` namespace
@ -4731,7 +4732,10 @@ export function createCssUtility(node: AtRule) {
return (designSystem: DesignSystem) => {
let valueThemeKeys = new Set<`--${string}`>()
let valueLiterals = new Set<string>()
let modifierThemeKeys = new Set<`--${string}`>()
let modifierLiterals = new Set<string>()
// Pre-process the AST to make it easier to work with.
//
@ -4747,12 +4751,12 @@ export function createCssUtility(node: AtRule) {
// Required manipulations:
//
// - `--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([*])`
// - `--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([*])`
//
// Once Prettier / Biome handle these better (e.g.: not crashing without
// `\\*` or not inserting whitespace) then most of these can go away.
@ -4783,9 +4787,25 @@ export function createCssUtility(node: AtRule) {
}
fn.nodes = ValueParser.parse(args.join(','))
// Track the theme keys for suggestions
// Track information for suggestions
for (let node of fn.nodes) {
if (node.kind === 'word' && node.value[0] === '-' && node.value[1] === '-') {
// Track literal values
if (
node.kind === 'word' &&
(node.value[0] === '"' || node.value[0] === "'") &&
node.value[0] === node.value[node.value.length - 1]
) {
let value = node.value.slice(1, -1)
if (fn.value === '--value') {
valueLiterals.add(value)
} else if (fn.value === '--modifier') {
modifierLiterals.add(value)
}
}
// Track theme keys
else if (node.kind === 'word' && node.value[0] === '-' && node.value[1] === '-') {
let value = node.value.replace(/-\*.*$/g, '') as `--${string}`
if (fn.value === '--value') {
@ -4929,16 +4949,23 @@ export function createCssUtility(node: AtRule) {
})
designSystem.utilities.suggest(name.slice(0, -2), () => {
return [
{
values: designSystem.theme
.keysInNamespaces(valueThemeKeys)
.map((x) => x.replaceAll('_', '.')),
modifiers: designSystem.theme
.keysInNamespaces(modifierThemeKeys)
.map((x) => x.replaceAll('_', '.')),
},
] satisfies SuggestionGroup[]
let values = []
for (let value of valueLiterals) {
values.push(value)
}
for (let value of designSystem.theme.keysInNamespaces(valueThemeKeys)) {
values.push(value)
}
let modifiers = []
for (let modifier of modifierLiterals) {
modifiers.push(modifier)
}
for (let value of designSystem.theme.keysInNamespaces(modifierThemeKeys)) {
modifiers.push(value)
}
return [{ values, modifiers }] satisfies SuggestionGroup[]
})
}
}
@ -4961,8 +4988,21 @@ function resolveValueFunction(
designSystem: DesignSystem,
): { nodes: ValueParser.ValueAstNode[]; ratio?: boolean } | undefined {
for (let arg of fn.nodes) {
// Resolving theme value, e.g.: `--value(--color)`
// Resolve literal value, e.g.: `--modifier('closest-side')`
if (
value.kind === 'named' &&
arg.kind === 'word' &&
// Should be wreapped in quotes
(arg.value[0] === "'" || arg.value[0] === '"') &&
arg.value[arg.value.length - 1] === arg.value[0] &&
// Values should match
arg.value.slice(1, -1) === value.value
) {
return { nodes: ValueParser.parse(value.value) }
}
// Resolving theme value, e.g.: `--value(--color)`
else if (
value.kind === 'named' &&
arg.kind === 'word' &&
arg.value[0] === '-' &&