Skip to content

Commit e000caa

Browse files
Add support for inline option when defining @theme values (#14095)
This PR adds support for a new `inline` option when defining a `@theme` block that tells Tailwind to use raw theme values for utilities instead of referencing the corresponding generated CSS variable. ```css /* Input */ @theme inline { --color-red-500: #ef4444; /* ... */ } /* Example output */ :root { --color-red-500: #ef4444; } .text-red-500 { color: #ef4444; } ``` This can be composed with the existing `reference` option in case you want to define a `@theme` block as both `reference` (so the variables aren't generated) and `inline`: ```css /* Input */ @theme inline reference { --color-red-500: #ef4444; /* ... */ } /* Example output */ .text-red-500 { color: #ef4444; } ``` Since you can have multiple `@theme` blocks, you can even define some values normally and some as inline based on how you're using them. For example you might want to use `inline` for defining literal tokens like `--color-red-500`, but include the variable for tokens that you want to be able to theme like `--color-primary`: ```css /* Input */ @theme inline { --color-red-500: #ef4444; /* ... */ } @theme { --color-primary: var(--color-red-500); } /* Example output */ :root { --color-red-500: #ef4444; --color-primary: var(--color-red-500); } .text-red-500 { color: #ef4444; } .text-primary { color: var(--color-primary, var(--color-red-500)); } ``` ## Breaking changes Prior to this PR, you could `@import` a stylesheet that contained `@theme` blocks as reference by adding the `reference` keyword to your import: ```css @import "./my-theme.css" reference; ``` Now that `reference` isn't the only possible option when declaring your `@theme`, this syntax has changed to a new `theme(…)` function that accepts `reference` and `inline` as potential space-separated values: ```css @import "./my-theme.css"; @import "./my-theme.css" theme(reference); @import "./my-theme.css" theme(inline); @import "./my-theme.css" theme(reference inline); ``` If you are using the `@import … reference` option with an earlier alpha release, you'll need to update your code to `@import … theme(reference)` once this PR lands in a release. ## Motivation This PR is designed to solve an issue pointed out in #14091. Prior to this PR, generated utilities would always reference variables directly, with the raw value as a fallback: ```css /* Input */ @theme { --color-red-500: #ef4444; /* ... */ } /* Example output */ :root { --color-red-500: #ef4444; } .text-red-500 { color: var(--color-red-500, #ef4444); } ``` But this can create issues with variables resolving to an unexpected value when a theme value is referencing another variable defined on `:root`. For example, say you have a CSS file like this: ```css :root, .light { --text-fg: #000; } .dark { --text-fg: #fff; } @theme { --color-fg: var(--text-fg); } ``` Without `@theme inline`, we'd generate this output if you used the `text-fg` utility: ```css :root, .light { --text-fg: #000; } .dark { --text-fg: #fff; } :root { --color-fg: var(--text-fg); } .text-fg { color: var(--color-fg, var(--text-fg)); } ``` Now if you wrote this HTML, you're probably expecting your text to be the dark mode color: ```html <div class="dark"> <h1 class="text-fg">Hello world</h1> </div> ``` But you'd actually get the light mode color because of this rule: ```css :root { --color-fg: var(--text-fg); } .text-fg { color: var(--color-fg, var(--text-fg)); } ``` The browser will try to resolve the `--color-fg` variable, which is defined on `:root`. When it tries to resolve the value, _it uses the value of `var(--text-fg)` as it would resolve at `:root`_, not what it would resolve to based on the element that has the `text-fg` class. So `var(--color-fg)` resolves to `#000` because `var(--text-fg)` resolved to `#000` at the point in the tree where the browser resolved the value of `var(--color-fg)`. By using `@theme inline`, the `.text-fg` class looks like this: ```css .text-fg { color: var(--text-fg); } ``` With this definition, the browser doesn't try to resolve `--color-fg` at all and instead resolves `--text-fg` directly which correctly resolves to `#fff` as expected. --------- Co-authored-by: Adam Wathan <[email protected]> Co-authored-by: Robin Malfait <[email protected]>
1 parent 27912f9 commit e000caa

File tree

5 files changed

+182
-21
lines changed

5 files changed

+182
-21
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Added
11+
12+
- Add support for `inline` option when defining `@theme` values ([#14095](https://github.com/tailwindlabs/tailwindcss/pull/14095))
1113

1214
## [4.0.0-alpha.18] - 2024-07-25
1315

packages/@tailwindcss-postcss/src/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ test('@apply can be used without emitting the theme in the CSS file', async () =
9494
// `@apply` is used.
9595
let result = await processor.process(
9696
css`
97-
@import 'tailwindcss/theme.css' reference;
97+
@import 'tailwindcss/theme.css' theme(reference);
9898
.foo {
9999
@apply text-red-500;
100100
}

packages/tailwindcss/src/index.test.ts

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,7 +1075,7 @@ describe('Parsing themes values from CSS', () => {
10751075
@theme {
10761076
--color-tomato: #e10c04;
10771077
}
1078-
@media reference {
1078+
@media theme(reference) {
10791079
@theme {
10801080
--color-potato: #ac855b;
10811081
}
@@ -1106,11 +1106,11 @@ describe('Parsing themes values from CSS', () => {
11061106
`)
11071107
})
11081108

1109-
test('`@media reference` can only contain `@theme` rules', () => {
1109+
test('`@media theme(…)` can only contain `@theme` rules', () => {
11101110
expect(() =>
11111111
compileCss(
11121112
css`
1113-
@media reference {
1113+
@media theme(reference) {
11141114
.not-a-theme-rule {
11151115
color: cursed;
11161116
}
@@ -1120,9 +1120,141 @@ describe('Parsing themes values from CSS', () => {
11201120
['bg-tomato', 'bg-potato', 'bg-avocado'],
11211121
),
11221122
).toThrowErrorMatchingInlineSnapshot(
1123-
`[Error: Files imported with \`@import "…" reference\` must only contain \`@theme\` blocks.]`,
1123+
`[Error: Files imported with \`@import "…" theme(…)\` must only contain \`@theme\` blocks.]`,
11241124
)
11251125
})
1126+
1127+
test('theme values added as `inline` are not wrapped in `var(…)` when used as utility values', () => {
1128+
expect(
1129+
compileCss(
1130+
css`
1131+
@theme inline {
1132+
--color-tomato: #e10c04;
1133+
--color-potato: #ac855b;
1134+
--color-primary: var(--primary);
1135+
}
1136+
1137+
@tailwind utilities;
1138+
`,
1139+
['bg-tomato', 'bg-potato', 'bg-primary'],
1140+
),
1141+
).toMatchInlineSnapshot(`
1142+
":root {
1143+
--color-tomato: #e10c04;
1144+
--color-potato: #ac855b;
1145+
--color-primary: var(--primary);
1146+
}
1147+
1148+
.bg-potato {
1149+
background-color: #ac855b;
1150+
}
1151+
1152+
.bg-primary {
1153+
background-color: var(--primary);
1154+
}
1155+
1156+
.bg-tomato {
1157+
background-color: #e10c04;
1158+
}"
1159+
`)
1160+
})
1161+
1162+
test('wrapping `@theme` with `@media theme(inline)` behaves like `@theme inline` to support `@import` statements', () => {
1163+
expect(
1164+
compileCss(
1165+
css`
1166+
@media theme(inline) {
1167+
@theme {
1168+
--color-tomato: #e10c04;
1169+
--color-potato: #ac855b;
1170+
--color-primary: var(--primary);
1171+
}
1172+
}
1173+
1174+
@tailwind utilities;
1175+
`,
1176+
['bg-tomato', 'bg-potato', 'bg-primary'],
1177+
),
1178+
).toMatchInlineSnapshot(`
1179+
":root {
1180+
--color-tomato: #e10c04;
1181+
--color-potato: #ac855b;
1182+
--color-primary: var(--primary);
1183+
}
1184+
1185+
.bg-potato {
1186+
background-color: #ac855b;
1187+
}
1188+
1189+
.bg-primary {
1190+
background-color: var(--primary);
1191+
}
1192+
1193+
.bg-tomato {
1194+
background-color: #e10c04;
1195+
}"
1196+
`)
1197+
})
1198+
1199+
test('`inline` and `reference` can be used together', () => {
1200+
expect(
1201+
compileCss(
1202+
css`
1203+
@theme reference inline {
1204+
--color-tomato: #e10c04;
1205+
--color-potato: #ac855b;
1206+
--color-primary: var(--primary);
1207+
}
1208+
1209+
@tailwind utilities;
1210+
`,
1211+
['bg-tomato', 'bg-potato', 'bg-primary'],
1212+
),
1213+
).toMatchInlineSnapshot(`
1214+
".bg-potato {
1215+
background-color: #ac855b;
1216+
}
1217+
1218+
.bg-primary {
1219+
background-color: var(--primary);
1220+
}
1221+
1222+
.bg-tomato {
1223+
background-color: #e10c04;
1224+
}"
1225+
`)
1226+
})
1227+
1228+
test('`inline` and `reference` can be used together in `media(…)`', () => {
1229+
expect(
1230+
compileCss(
1231+
css`
1232+
@media theme(reference inline) {
1233+
@theme {
1234+
--color-tomato: #e10c04;
1235+
--color-potato: #ac855b;
1236+
--color-primary: var(--primary);
1237+
}
1238+
}
1239+
1240+
@tailwind utilities;
1241+
`,
1242+
['bg-tomato', 'bg-potato', 'bg-primary'],
1243+
),
1244+
).toMatchInlineSnapshot(`
1245+
".bg-potato {
1246+
background-color: #ac855b;
1247+
}
1248+
1249+
.bg-primary {
1250+
background-color: var(--primary);
1251+
}
1252+
1253+
.bg-tomato {
1254+
background-color: #e10c04;
1255+
}"
1256+
`)
1257+
})
11261258
})
11271259

11281260
describe('plugins', () => {

packages/tailwindcss/src/index.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ function throwOnPlugin(): never {
3333
throw new Error('No `loadPlugin` function provided to `compile`')
3434
}
3535

36+
function parseThemeOptions(selector: string) {
37+
let isReference = false
38+
let isInline = false
39+
40+
for (let option of segment(selector.slice(6) /* '@theme'.length */, ' ')) {
41+
if (option === 'reference') {
42+
isReference = true
43+
} else if (option === 'inline') {
44+
isInline = true
45+
}
46+
}
47+
48+
return { isReference, isInline }
49+
}
50+
3651
export function compile(
3752
css: string,
3853
{ loadPlugin = throwOnPlugin }: CompileOptions = {},
@@ -152,28 +167,32 @@ export function compile(
152167
}
153168
}
154169

155-
// Drop instances of `@media reference`
170+
// Drop instances of `@media theme(…)`
156171
//
157-
// We support `@import "tailwindcss/theme" reference` as a way to import an external theme file
158-
// as a reference, which becomes `@media reference { … }` when the `@import` is processed.
159-
if (node.selector === '@media reference') {
172+
// We support `@import "tailwindcss/theme" theme(reference)` as a way to
173+
// import an external theme file as a reference, which becomes `@media
174+
// theme(reference) { … }` when the `@import` is processed.
175+
if (node.selector.startsWith('@media theme(')) {
176+
let themeParams = node.selector.slice(13, -1)
177+
160178
walk(node.nodes, (child) => {
161179
if (child.kind !== 'rule') {
162180
throw new Error(
163-
'Files imported with `@import "…" reference` must only contain `@theme` blocks.',
181+
'Files imported with `@import "…" theme(…)` must only contain `@theme` blocks.',
164182
)
165183
}
166184
if (child.selector === '@theme') {
167-
child.selector = '@theme reference'
185+
child.selector = '@theme ' + themeParams
168186
return WalkAction.Skip
169187
}
170188
})
171189
replaceWith(node.nodes)
190+
return WalkAction.Skip
172191
}
173192

174-
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
193+
if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return
175194

176-
let isReference = node.selector === '@theme reference'
195+
let { isReference, isInline } = parseThemeOptions(node.selector)
177196

178197
// Record all custom properties in the `@theme` declaration
179198
walk(node.nodes, (child, { replaceWith }) => {
@@ -187,7 +206,7 @@ export function compile(
187206

188207
if (child.kind === 'comment') return
189208
if (child.kind === 'declaration' && child.property.startsWith('--')) {
190-
theme.add(child.property, child.value, isReference)
209+
theme.add(child.property, child.value, { isReference, isInline })
191210
return
192211
}
193212

@@ -395,15 +414,15 @@ export function __unstable__loadDesignSystem(css: string) {
395414

396415
walk(ast, (node) => {
397416
if (node.kind !== 'rule') return
398-
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
399-
let isReference = node.selector === '@theme reference'
417+
if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return
418+
let { isReference, isInline } = parseThemeOptions(node.selector)
400419

401420
// Record all custom properties in the `@theme` declaration
402421
walk([node], (node) => {
403422
if (node.kind !== 'declaration') return
404423
if (!node.property.startsWith('--')) return
405424

406-
theme.add(node.property, node.value, isReference)
425+
theme.add(node.property, node.value, { isReference, isInline })
407426
})
408427
})
409428

packages/tailwindcss/src/theme.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { escape } from './utils/escape'
22

33
export class Theme {
4-
constructor(private values = new Map<string, { value: string; isReference: boolean }>()) {}
4+
constructor(
5+
private values = new Map<string, { value: string; isReference: boolean; isInline: boolean }>(),
6+
) {}
57

6-
add(key: string, value: string, isReference = false): void {
8+
add(key: string, value: string, { isReference = false, isInline = false } = {}): void {
79
if (key.endsWith('-*')) {
810
if (value !== 'initial') {
911
throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``)
@@ -18,7 +20,7 @@ export class Theme {
1820
if (value === 'initial') {
1921
this.values.delete(key)
2022
} else {
21-
this.values.set(key, { value, isReference })
23+
this.values.set(key, { value, isReference, isInline })
2224
}
2325
}
2426

@@ -91,6 +93,12 @@ export class Theme {
9193

9294
if (!themeKey) return null
9395

96+
let value = this.values.get(themeKey)!
97+
98+
if (value.isInline) {
99+
return value.value
100+
}
101+
94102
return this.#var(themeKey)
95103
}
96104

0 commit comments

Comments
 (0)