Skip to content

Commit 60e6195

Browse files
Ensure escaped theme variables are handled correctly (#16064)
This PR ensures that escaped theme variables are properly handled. We do this by moving the `escape`/`unescape` responsibility back into the main tailwindcss entrypoint that reads and writes from the CSS and making sure that _all internal state of the `Theme` class are unescaped classes. However, due to us accidentally shipping the part where a dot in the theme variable would translate to an underscore in CSS already, this logic is going to stay as-is for now. Here's an example test that visualizes the new changes: ```ts expect( await compileCss( css` @theme { --spacing-*: initial; --spacing-1\.5: 2.5rem; --spacing-foo\/bar: 3rem; } @tailwind utilities; `, ['m-1.5', 'm-foo/bar'], ), ).toMatchInlineSnapshot(` ":root, :host { --spacing-1\.5: 2.5rem; --spacing-foo\\/bar: 3rem; } .m-1\\.5 { margin: var(--spacing-1\.5); } .m-foo\\/bar { margin: var(--spacing-foo\\/bar); }" `) ``` ## Test plan - Added a unit test - Ensure this works end-to-end using the Vite playground: <img width="1016" alt="Screenshot 2025-01-30 at 14 51 05" src="https://github.com/user-attachments/assets/463c6fd5-793f-4ecc-86d2-5ad40bbb3e74" />
1 parent 3aa0e49 commit 60e6195

File tree

5 files changed

+62
-13
lines changed

5 files changed

+62
-13
lines changed

CHANGELOG.md

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

1212
- Only generate positive `grid-cols-*` and `grid-rows-*` utilities ([#16020](https://github.com/tailwindlabs/tailwindcss/pull/16020))
13+
- Ensure escaped theme variables are handled correctly ([#16064](https://github.com/tailwindlabs/tailwindcss/pull/16064))
1314
- Ensure we process Tailwind CSS features when only using `@reference` or `@variant` ([#16057](https://github.com/tailwindlabs/tailwindcss/pull/16057))
1415
- Refactor gradient implementation to work around [prettier/prettier#17058](https://github.com/prettier/prettier/issues/17058) ([#16072](https://github.com/tailwindlabs/tailwindcss/pull/16072))
1516
- Vite: Ensure hot-reloading works with SolidStart setups ([#16052](https://github.com/tailwindlabs/tailwindcss/pull/16052))

packages/tailwindcss/src/compat/apply-config-to-theme.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { DesignSystem } from '../design-system'
22
import { ThemeOptions } from '../theme'
3-
import { escape } from '../utils/escape'
43
import type { ResolvedConfig } from './config/types'
54

65
function resolveThemeValue(value: unknown, subValue: string | null = null): string | null {
@@ -55,7 +54,7 @@ export function applyConfigToTheme(
5554
if (!name) continue
5655

5756
designSystem.theme.add(
58-
`--${escape(name)}`,
57+
`--${name}`,
5958
'' + value,
6059
ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT,
6160
)

packages/tailwindcss/src/index.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,49 @@ describe('compiling CSS', () => {
152152
`)
153153
})
154154

155+
test('unescapes theme variables and handles dots as underscore', async () => {
156+
expect(
157+
await compileCss(
158+
css`
159+
@theme {
160+
--spacing-*: initial;
161+
--spacing-1\.5: 1.5px;
162+
--spacing-2_5: 2.5px;
163+
--spacing-3\.5: 3.5px;
164+
--spacing-3_5: 3.5px;
165+
--spacing-foo\/bar: 3rem;
166+
}
167+
@tailwind utilities;
168+
`,
169+
['m-1.5', 'm-2.5', 'm-2_5', 'm-3.5', 'm-foo/bar'],
170+
),
171+
).toMatchInlineSnapshot(`
172+
":root, :host {
173+
--spacing-1\\.5: 1.5px;
174+
--spacing-2_5: 2.5px;
175+
--spacing-3\\.5: 3.5px;
176+
--spacing-3_5: 3.5px;
177+
--spacing-foo\\/bar: 3rem;
178+
}
179+
180+
.m-1\\.5 {
181+
margin: var(--spacing-1\\.5);
182+
}
183+
184+
.m-2\\.5, .m-2_5 {
185+
margin: var(--spacing-2_5);
186+
}
187+
188+
.m-3\\.5 {
189+
margin: var(--spacing-3\\.5);
190+
}
191+
192+
.m-foo\\/bar {
193+
margin: var(--spacing-foo\\/bar);
194+
}"
195+
`)
196+
})
197+
155198
test('adds vendor prefixes', async () => {
156199
expect(
157200
await compileCss(

packages/tailwindcss/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import * as CSS from './css-parser'
2727
import { buildDesignSystem, type DesignSystem } from './design-system'
2828
import { Theme, ThemeOptions } from './theme'
2929
import { createCssUtility } from './utilities'
30+
import { escape, unescape } from './utils/escape'
3031
import { segment } from './utils/segment'
3132
import { compoundsForSelectors, IS_VALID_VARIANT_NAME } from './variants'
3233
export type Config = UserConfig
@@ -467,7 +468,7 @@ async function parseCss(
467468

468469
if (child.kind === 'comment') return
469470
if (child.kind === 'declaration' && child.property.startsWith('--')) {
470-
theme.add(child.property, child.value ?? '', themeOptions)
471+
theme.add(unescape(child.property), child.value ?? '', themeOptions)
471472
return
472473
}
473474

@@ -526,7 +527,7 @@ async function parseCss(
526527

527528
for (let [key, value] of theme.entries()) {
528529
if (value.options & ThemeOptions.REFERENCE) continue
529-
nodes.push(decl(key, value.value))
530+
nodes.push(decl(escape(key), value.value))
530531
}
531532

532533
let keyframesRules = theme.getKeyframes()

packages/tailwindcss/src/theme.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@ export class Theme {
4141
) {}
4242

4343
add(key: string, value: string, options = ThemeOptions.NONE): void {
44-
if (key.endsWith('\\*')) {
45-
key = key.slice(0, -2) + '*'
46-
}
47-
4844
if (key.endsWith('-*')) {
4945
if (value !== 'initial') {
5046
throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``)
@@ -149,11 +145,20 @@ export class Theme {
149145
#resolveKey(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
150146
for (let namespace of themeKeys) {
151147
let themeKey =
152-
candidateValue !== null
153-
? (escape(`${namespace}-${candidateValue.replaceAll('.', '_')}`) as ThemeKey)
154-
: namespace
148+
candidateValue !== null ? (`${namespace}-${candidateValue}` as ThemeKey) : namespace
149+
150+
if (!this.values.has(themeKey)) {
151+
// If the exact theme key is not found, we might be trying to resolve a key containing a dot
152+
// that was registered with an underscore instead:
153+
if (candidateValue !== null && candidateValue.includes('.')) {
154+
themeKey = `${namespace}-${candidateValue.replaceAll('.', '_')}` as ThemeKey
155+
156+
if (!this.values.has(themeKey)) continue
157+
} else {
158+
continue
159+
}
160+
}
155161

156-
if (!this.values.has(themeKey)) continue
157162
if (isIgnoredThemeKey(themeKey, namespace)) continue
158163

159164
return themeKey
@@ -167,7 +172,7 @@ export class Theme {
167172
return null
168173
}
169174

170-
return `var(${this.#prefixKey(themeKey)})`
175+
return `var(${escape(this.#prefixKey(themeKey))})`
171176
}
172177

173178
resolve(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {

0 commit comments

Comments
 (0)