Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions packages/tailwindcss/src/intellisense.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-* {
Expand All @@ -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')
Expand All @@ -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 () => {
Expand Down
37 changes: 36 additions & 1 deletion packages/tailwindcss/src/utilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-* {
Expand Down Expand Up @@ -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;
Expand All @@ -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\\] {
Expand All @@ -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;
}
Expand All @@ -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('')
})

Expand Down
78 changes: 59 additions & 19 deletions packages/tailwindcss/src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
//
Expand All @@ -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.
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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[]
})
}
}
Expand All @@ -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] === '-' &&
Expand Down