diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d3513c1823..bc308f6c655c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 4a69aa5009ff..1166b1a73621 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -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 () => { diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 21b43dabab0e..908a2398ae4d 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -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('') }) diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 587dc2d508a8..71c53de85912 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -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() + let modifierThemeKeys = new Set<`--${string}`>() + let modifierLiterals = new Set() // 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] === '-' &&