Skip to content

Commit d832cea

Browse files
Force-inline when --theme(…) is used inside at rules
1 parent 7bd003f commit d832cea

File tree

2 files changed

+107
-20
lines changed

2 files changed

+107
-20
lines changed

packages/tailwindcss/src/css-functions.test.ts

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,26 +152,70 @@ describe('--theme(…)', () => {
152152
color: --theme(--color-red-500);
153153
}
154154
`),
155+
).toMatchInlineSnapshot(`
156+
":root, :host {
157+
--color-red-500: red;
158+
}
159+
160+
.red {
161+
color: var(--color-red-500);
162+
}"
163+
`)
164+
})
165+
166+
test('--theme(--color-red-500 inline)', async () => {
167+
expect(
168+
await compileCss(css`
169+
@theme {
170+
--color-red-500: #f00;
171+
}
172+
.red {
173+
color: --theme(--color-red-500 inline);
174+
}
175+
`),
155176
).toMatchInlineSnapshot(`
156177
".red {
157178
color: red;
158179
}"
159180
`)
160181
})
161182

162-
test('--theme(…) can only be used with CSS variables from your theme', async () => {
163-
expect(() =>
164-
compileCss(css`
183+
test('--theme(--color-red-500/50)', async () => {
184+
expect(
185+
await compileCss(css`
165186
@theme {
166187
--color-red-500: #f00;
167188
}
168189
.red {
169-
color: --theme(colors.red.500);
190+
color: --theme(--color-red-500/0.5);
170191
}
171192
`),
172-
).rejects.toThrowErrorMatchingInlineSnapshot(
173-
`[Error: The --theme(…) function can only be used with CSS variables from your theme.]`,
174-
)
193+
).toMatchInlineSnapshot(`
194+
":root, :host {
195+
--color-red-500: red;
196+
}
197+
198+
.red {
199+
color: color-mix(in oklab, var(--color-red-500) 50%, transparent);
200+
}"
201+
`)
202+
})
203+
204+
test('--theme(--color-red-500/50 inline)', async () => {
205+
expect(
206+
await compileCss(css`
207+
@theme {
208+
--color-red-500: #f00;
209+
}
210+
.red {
211+
color: --theme(--color-red-500/50 inline);
212+
}
213+
`),
214+
).toMatchInlineSnapshot(`
215+
".red {
216+
color: oklab(62.7955% .224863 .125846);
217+
}"
218+
`)
175219
})
176220

177221
test('--theme(…) forces the value to be retrieved as inline when used inside an at rule', async () => {
@@ -207,6 +251,21 @@ describe('--theme(…)', () => {
207251
}"
208252
`)
209253
})
254+
255+
test('--theme(…) can only be used with CSS variables from your theme', async () => {
256+
expect(() =>
257+
compileCss(css`
258+
@theme {
259+
--color-red-500: #f00;
260+
}
261+
.red {
262+
color: --theme(colors.red.500);
263+
}
264+
`),
265+
).rejects.toThrowErrorMatchingInlineSnapshot(
266+
`[Error: The --theme(…) function can only be used with CSS variables from your theme.]`,
267+
)
268+
})
210269
})
211270

212271
describe('theme(…)', () => {

packages/tailwindcss/src/css-functions.ts

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ import { withAlpha } from './utilities'
55
import { segment } from './utils/segment'
66
import * as ValueParser from './value-parser'
77

8-
const functions: Record<string, (designSystem: DesignSystem, ...args: string[]) => string> = {
8+
const CSS_FUNCTIONS: Record<
9+
string,
10+
(designSystem: DesignSystem, source: AstNode, ...args: string[]) => string
11+
> = {
912
'--alpha': alpha,
1013
'--spacing': spacing,
1114
'--theme': theme,
1215
theme: legacyTheme,
1316
}
1417

15-
function alpha(_designSystem: DesignSystem, value: string, ...rest: string[]) {
18+
function alpha(_designSystem: DesignSystem, _source: AstNode, value: string, ...rest: string[]) {
1619
let [color, alpha] = segment(value, '/').map((v) => v.trim())
1720

1821
if (!color || !alpha) {
@@ -30,7 +33,7 @@ function alpha(_designSystem: DesignSystem, value: string, ...rest: string[]) {
3033
return withAlpha(color, alpha)
3134
}
3235

33-
function spacing(designSystem: DesignSystem, value: string, ...rest: string[]) {
36+
function spacing(designSystem: DesignSystem, _source: AstNode, value: string, ...rest: string[]) {
3437
if (!value) {
3538
throw new Error(`The --spacing(…) function requires an argument, but received none.`)
3639
}
@@ -51,27 +54,44 @@ function spacing(designSystem: DesignSystem, value: string, ...rest: string[]) {
5154
return `calc(${multiplier} * ${value})`
5255
}
5356

54-
function theme(designSystem: DesignSystem, path: string, ...fallback: string[]) {
57+
function theme(designSystem: DesignSystem, source: AstNode, path: string, ...fallback: string[]) {
5558
if (!path.startsWith('--')) {
5659
throw new Error(`The --theme(…) function can only be used with CSS variables from your theme.`)
5760
}
5861

59-
let resolvedValue = designSystem.resolveThemeValue(path)
62+
let inline = false
63+
if (path.endsWith(' inline')) {
64+
inline = true
65+
path = path.slice(0, -7)
66+
}
67+
68+
// If the `--theme(…)` function is used within an at-rule (e.g. `@media (width >= theme(…)))`, we
69+
// have to always inline the result since CSS does not support CSS variables in these positions.
70+
if (source.kind === 'at-rule') {
71+
inline = true
72+
}
73+
74+
let resolvedValue = designSystem.resolveThemeValue(path, inline)
6075

6176
if (!resolvedValue && fallback.length > 0) {
6277
return fallback.join(', ')
6378
}
6479

6580
if (!resolvedValue) {
6681
throw new Error(
67-
`Could not resolve value for theme function: \`theme(${path})\`. Consider checking if the path is correct or provide a fallback value to silence this error.`,
82+
`Could not resolve value for theme function: \`theme(${path})\`. Consider checking if the variable name is correct or provide a fallback value to silence this error.`,
6883
)
6984
}
7085

7186
return resolvedValue
7287
}
7388

74-
function legacyTheme(designSystem: DesignSystem, path: string, ...fallback: string[]) {
89+
function legacyTheme(
90+
designSystem: DesignSystem,
91+
_source: AstNode,
92+
path: string,
93+
...fallback: string[]
94+
) {
7595
path = eventuallyUnquote(path)
7696

7797
let resolvedValue = designSystem.resolveThemeValue(path)
@@ -90,7 +110,7 @@ function legacyTheme(designSystem: DesignSystem, path: string, ...fallback: stri
90110
}
91111

92112
export const THEME_FUNCTION_INVOCATION = new RegExp(
93-
Object.keys(functions)
113+
Object.keys(CSS_FUNCTIONS)
94114
.map((x) => `${x}\\(`)
95115
.join('|'),
96116
)
@@ -101,7 +121,7 @@ export function substituteFunctions(ast: AstNode[], designSystem: DesignSystem)
101121
// Find all declaration values
102122
if (node.kind === 'declaration' && node.value && THEME_FUNCTION_INVOCATION.test(node.value)) {
103123
features |= Features.ThemeFunction
104-
node.value = substituteFunctionsInValue(node.value, designSystem)
124+
node.value = substituteFunctionsInValue(node.value, node, designSystem)
105125
return
106126
}
107127

@@ -115,19 +135,27 @@ export function substituteFunctions(ast: AstNode[], designSystem: DesignSystem)
115135
THEME_FUNCTION_INVOCATION.test(node.params)
116136
) {
117137
features |= Features.ThemeFunction
118-
node.params = substituteFunctionsInValue(node.params, designSystem)
138+
node.params = substituteFunctionsInValue(node.params, node, designSystem)
119139
}
120140
}
121141
})
122142
return features
123143
}
124144

125-
export function substituteFunctionsInValue(value: string, designSystem: DesignSystem): string {
145+
export function substituteFunctionsInValue(
146+
value: string,
147+
source: AstNode,
148+
designSystem: DesignSystem,
149+
): string {
126150
let ast = ValueParser.parse(value)
127151
ValueParser.walk(ast, (node, { replaceWith }) => {
128-
if (node.kind === 'function' && node.value in functions) {
152+
if (node.kind === 'function' && node.value in CSS_FUNCTIONS) {
129153
let args = segment(ValueParser.toCss(node.nodes).trim(), ',').map((x) => x.trim())
130-
let result = functions[node.value as keyof typeof functions](designSystem, ...args)
154+
let result = CSS_FUNCTIONS[node.value as keyof typeof CSS_FUNCTIONS](
155+
designSystem,
156+
source,
157+
...args,
158+
)
131159
return replaceWith(ValueParser.parse(result))
132160
}
133161
})

0 commit comments

Comments
 (0)