Skip to content

Commit 9fd6766

Browse files
Ensure first argument to var(…) still unescapes \_ (#16206)
Resolves #16170 This PR fixes an issue where the previously opted-out escaping of the first argument for the `var(…)` function was not unescaped at all. This was introduced in #14776 where the intention was to not require escaping of underscores in the var function (e.g. `ml-[var(--spacing-1_5)]`). However, I do think it still makes sense to unescape an eventually escaped underline for consistency. ## Test plan The example from #1670 now parses as expected: <img width="904" alt="Screenshot 2025-02-03 at 13 51 35" src="https://github.com/user-attachments/assets/cac0f06e-37da-4dcb-a554-9606d144a8d5" /> --------- Co-authored-by: Robin Malfait <[email protected]>
1 parent e1a85ac commit 9fd6766

File tree

6 files changed

+157
-10
lines changed

6 files changed

+157
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- Ensure CSS variables in arbitrary values are properly decoded ([#16206](https://github.com/tailwindlabs/tailwindcss/pull/16206))
1213
- Ensure that the `containers` JS theme key is added to the `--container-*` namespace ([#16169](https://github.com/tailwindlabs/tailwindcss/pull/16169))
1314
- Fix missing `@keyframes` definition ([#16237](https://github.com/tailwindlabs/tailwindcss/pull/16237))
1415
- Vite: Skip parsing stylesheets with the `?commonjs-proxy` flag ([#16238](https://github.com/tailwindlabs/tailwindcss/pull/16238))

packages/@tailwindcss-upgrade/src/template/candidates.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ const candidates = [
110110
['!bg-[#0088cc]', 'bg-[#0088cc]!'],
111111
['bg-[var(--spacing)-1px]', 'bg-[var(--spacing)-1px]'],
112112
['bg-[var(--spacing)_-_1px]', 'bg-[var(--spacing)-1px]'],
113+
['bg-[var(--_spacing)]', 'bg-(--_spacing)'],
114+
['bg-(--_spacing)', 'bg-(--_spacing)'],
115+
['bg-[var(--\_spacing)]', 'bg-(--_spacing)'],
116+
['bg-(--\_spacing)', 'bg-(--_spacing)'],
113117
['bg-[-1px_-1px]', 'bg-[-1px_-1px]'],
114118
['p-[round(to-zero,1px)]', 'p-[round(to-zero,1px)]'],
115119
['w-1/2', 'w-1/2'],

packages/@tailwindcss-upgrade/src/template/candidates.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,13 +241,8 @@ function recursivelyEscapeUnderscores(ast: ValueParser.ValueAstNode[]) {
241241
node.value === 'theme' ||
242242
node.value.endsWith('_theme')
243243
) {
244-
// Don't decode underscores in the first argument of var() and theme()
245-
// but do decode the function name
246244
node.value = escapeUnderscore(node.value)
247245
for (let i = 0; i < node.nodes.length; i++) {
248-
if (i == 0 && node.nodes[i].kind === 'word') {
249-
continue
250-
}
251246
recursivelyEscapeUnderscores([node.nodes[i]])
252247
}
253248
break

packages/tailwindcss/src/candidate.test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,154 @@ it('should parse a utility with an implicit variable as the modifier', () => {
935935
`)
936936
})
937937

938+
it('should properly decode escaped underscores but not convert underscores to spaces for CSS variables in arbitrary positions', () => {
939+
let utilities = new Utilities()
940+
utilities.functional('flex', () => [])
941+
let variants = new Variants()
942+
variants.functional('supports', () => {})
943+
944+
expect(run('flex-(--\\_foo)', { utilities, variants })).toMatchInlineSnapshot(`
945+
[
946+
{
947+
"important": false,
948+
"kind": "functional",
949+
"modifier": null,
950+
"raw": "flex-(--\\_foo)",
951+
"root": "flex",
952+
"value": {
953+
"dataType": null,
954+
"kind": "arbitrary",
955+
"value": "var(--_foo)",
956+
},
957+
"variants": [],
958+
},
959+
]
960+
`)
961+
expect(run('flex-(--_foo)', { utilities, variants })).toMatchInlineSnapshot(`
962+
[
963+
{
964+
"important": false,
965+
"kind": "functional",
966+
"modifier": null,
967+
"raw": "flex-(--_foo)",
968+
"root": "flex",
969+
"value": {
970+
"dataType": null,
971+
"kind": "arbitrary",
972+
"value": "var(--_foo)",
973+
},
974+
"variants": [],
975+
},
976+
]
977+
`)
978+
expect(run('flex-[var(--\\_foo)]', { utilities, variants })).toMatchInlineSnapshot(`
979+
[
980+
{
981+
"important": false,
982+
"kind": "functional",
983+
"modifier": null,
984+
"raw": "flex-[var(--\\_foo)]",
985+
"root": "flex",
986+
"value": {
987+
"dataType": null,
988+
"kind": "arbitrary",
989+
"value": "var(--_foo)",
990+
},
991+
"variants": [],
992+
},
993+
]
994+
`)
995+
expect(run('flex-[var(--_foo)]', { utilities, variants })).toMatchInlineSnapshot(`
996+
[
997+
{
998+
"important": false,
999+
"kind": "functional",
1000+
"modifier": null,
1001+
"raw": "flex-[var(--_foo)]",
1002+
"root": "flex",
1003+
"value": {
1004+
"dataType": null,
1005+
"kind": "arbitrary",
1006+
"value": "var(--_foo)",
1007+
},
1008+
"variants": [],
1009+
},
1010+
]
1011+
`)
1012+
1013+
expect(run('flex-[calc(var(--\\_foo)*0.2)]', { utilities, variants })).toMatchInlineSnapshot(`
1014+
[
1015+
{
1016+
"important": false,
1017+
"kind": "functional",
1018+
"modifier": null,
1019+
"raw": "flex-[calc(var(--\\_foo)*0.2)]",
1020+
"root": "flex",
1021+
"value": {
1022+
"dataType": null,
1023+
"kind": "arbitrary",
1024+
"value": "calc(var(--_foo) * 0.2)",
1025+
},
1026+
"variants": [],
1027+
},
1028+
]
1029+
`)
1030+
expect(run('flex-[calc(var(--_foo)*0.2)]', { utilities, variants })).toMatchInlineSnapshot(`
1031+
[
1032+
{
1033+
"important": false,
1034+
"kind": "functional",
1035+
"modifier": null,
1036+
"raw": "flex-[calc(var(--_foo)*0.2)]",
1037+
"root": "flex",
1038+
"value": {
1039+
"dataType": null,
1040+
"kind": "arbitrary",
1041+
"value": "calc(var(--_foo) * 0.2)",
1042+
},
1043+
"variants": [],
1044+
},
1045+
]
1046+
`)
1047+
1048+
// Due to limitations in the CSS value parser, the `var(…)` inside the `calc(…)` is not correctly
1049+
// scanned here.
1050+
expect(run('flex-[calc(0.2*var(--\\_foo)]', { utilities, variants })).toMatchInlineSnapshot(`
1051+
[
1052+
{
1053+
"important": false,
1054+
"kind": "functional",
1055+
"modifier": null,
1056+
"raw": "flex-[calc(0.2*var(--\\_foo)]",
1057+
"root": "flex",
1058+
"value": {
1059+
"dataType": null,
1060+
"kind": "arbitrary",
1061+
"value": "calc(0.2 * var(--_foo))",
1062+
},
1063+
"variants": [],
1064+
},
1065+
]
1066+
`)
1067+
expect(run('flex-[calc(0.2*var(--_foo)]', { utilities, variants })).toMatchInlineSnapshot(`
1068+
[
1069+
{
1070+
"important": false,
1071+
"kind": "functional",
1072+
"modifier": null,
1073+
"raw": "flex-[calc(0.2*var(--_foo)]",
1074+
"root": "flex",
1075+
"value": {
1076+
"dataType": null,
1077+
"kind": "arbitrary",
1078+
"value": "calc(0.2 * var(-- foo))",
1079+
},
1080+
"variants": [],
1081+
},
1082+
]
1083+
`)
1084+
})
1085+
9381086
it('should parse a utility with an implicit variable as the modifier using the shorthand', () => {
9391087
let utilities = new Utilities()
9401088
utilities.functional('bg', () => [])

packages/tailwindcss/src/utils/decode-arbitrary-value.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function decodeArbitraryValue(input: string): string {
2020
* Convert `_` to ` `, except for escaped underscores `\_` they should be
2121
* converted to `_` instead.
2222
*/
23-
function convertUnderscoresToWhitespace(input: string) {
23+
function convertUnderscoresToWhitespace(input: string, skipUnderscoreToSpace = false) {
2424
let output = ''
2525
for (let i = 0; i < input.length; i++) {
2626
let char = input[i]
@@ -32,7 +32,7 @@ function convertUnderscoresToWhitespace(input: string) {
3232
}
3333

3434
// Unescaped underscore
35-
else if (char === '_') {
35+
else if (char === '_' && !skipUnderscoreToSpace) {
3636
output += ' '
3737
}
3838

@@ -61,11 +61,11 @@ function recursivelyDecodeArbitraryValues(ast: ValueParser.ValueAstNode[]) {
6161
node.value === 'theme' ||
6262
node.value.endsWith('_theme')
6363
) {
64-
// Don't decode underscores in the first argument of var() but do
65-
// decode the function name
6664
node.value = convertUnderscoresToWhitespace(node.value)
6765
for (let i = 0; i < node.nodes.length; i++) {
66+
// Don't decode underscores to spaces in the first argument of var()
6867
if (i == 0 && node.nodes[i].kind === 'word') {
68+
node.nodes[i].value = convertUnderscoresToWhitespace(node.nodes[i].value, true)
6969
continue
7070
}
7171
recursivelyDecodeArbitraryValues([node.nodes[i]])

playgrounds/vite/src/app.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ export function App() {
22
return (
33
<div className="m-3 p-3 border">
44
<h1 className="text-blue-500">Hello World</h1>
5-
<div className="-inset-x-full -inset-y-full -space-x-full -space-y-full -inset-full"></div>
65
</div>
76
)
87
}

0 commit comments

Comments
 (0)