mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
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 <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
27912f9bb5
commit
e000caa0bd
@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Nothing yet!
|
||||
### Added
|
||||
|
||||
- Add support for `inline` option when defining `@theme` values ([#14095](https://github.com/tailwindlabs/tailwindcss/pull/14095))
|
||||
|
||||
## [4.0.0-alpha.18] - 2024-07-25
|
||||
|
||||
|
||||
@ -94,7 +94,7 @@ test('@apply can be used without emitting the theme in the CSS file', async () =
|
||||
// `@apply` is used.
|
||||
let result = await processor.process(
|
||||
css`
|
||||
@import 'tailwindcss/theme.css' reference;
|
||||
@import 'tailwindcss/theme.css' theme(reference);
|
||||
.foo {
|
||||
@apply text-red-500;
|
||||
}
|
||||
|
||||
@ -1075,7 +1075,7 @@ describe('Parsing themes values from CSS', () => {
|
||||
@theme {
|
||||
--color-tomato: #e10c04;
|
||||
}
|
||||
@media reference {
|
||||
@media theme(reference) {
|
||||
@theme {
|
||||
--color-potato: #ac855b;
|
||||
}
|
||||
@ -1106,11 +1106,11 @@ describe('Parsing themes values from CSS', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
test('`@media reference` can only contain `@theme` rules', () => {
|
||||
test('`@media theme(…)` can only contain `@theme` rules', () => {
|
||||
expect(() =>
|
||||
compileCss(
|
||||
css`
|
||||
@media reference {
|
||||
@media theme(reference) {
|
||||
.not-a-theme-rule {
|
||||
color: cursed;
|
||||
}
|
||||
@ -1120,9 +1120,141 @@ describe('Parsing themes values from CSS', () => {
|
||||
['bg-tomato', 'bg-potato', 'bg-avocado'],
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: Files imported with \`@import "…" reference\` must only contain \`@theme\` blocks.]`,
|
||||
`[Error: Files imported with \`@import "…" theme(…)\` must only contain \`@theme\` blocks.]`,
|
||||
)
|
||||
})
|
||||
|
||||
test('theme values added as `inline` are not wrapped in `var(…)` when used as utility values', () => {
|
||||
expect(
|
||||
compileCss(
|
||||
css`
|
||||
@theme inline {
|
||||
--color-tomato: #e10c04;
|
||||
--color-potato: #ac855b;
|
||||
--color-primary: var(--primary);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['bg-tomato', 'bg-potato', 'bg-primary'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--color-tomato: #e10c04;
|
||||
--color-potato: #ac855b;
|
||||
--color-primary: var(--primary);
|
||||
}
|
||||
|
||||
.bg-potato {
|
||||
background-color: #ac855b;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.bg-tomato {
|
||||
background-color: #e10c04;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('wrapping `@theme` with `@media theme(inline)` behaves like `@theme inline` to support `@import` statements', () => {
|
||||
expect(
|
||||
compileCss(
|
||||
css`
|
||||
@media theme(inline) {
|
||||
@theme {
|
||||
--color-tomato: #e10c04;
|
||||
--color-potato: #ac855b;
|
||||
--color-primary: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['bg-tomato', 'bg-potato', 'bg-primary'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--color-tomato: #e10c04;
|
||||
--color-potato: #ac855b;
|
||||
--color-primary: var(--primary);
|
||||
}
|
||||
|
||||
.bg-potato {
|
||||
background-color: #ac855b;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.bg-tomato {
|
||||
background-color: #e10c04;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('`inline` and `reference` can be used together', () => {
|
||||
expect(
|
||||
compileCss(
|
||||
css`
|
||||
@theme reference inline {
|
||||
--color-tomato: #e10c04;
|
||||
--color-potato: #ac855b;
|
||||
--color-primary: var(--primary);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['bg-tomato', 'bg-potato', 'bg-primary'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
".bg-potato {
|
||||
background-color: #ac855b;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.bg-tomato {
|
||||
background-color: #e10c04;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('`inline` and `reference` can be used together in `media(…)`', () => {
|
||||
expect(
|
||||
compileCss(
|
||||
css`
|
||||
@media theme(reference inline) {
|
||||
@theme {
|
||||
--color-tomato: #e10c04;
|
||||
--color-potato: #ac855b;
|
||||
--color-primary: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['bg-tomato', 'bg-potato', 'bg-primary'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
".bg-potato {
|
||||
background-color: #ac855b;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.bg-tomato {
|
||||
background-color: #e10c04;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('plugins', () => {
|
||||
|
||||
@ -33,6 +33,21 @@ function throwOnPlugin(): never {
|
||||
throw new Error('No `loadPlugin` function provided to `compile`')
|
||||
}
|
||||
|
||||
function parseThemeOptions(selector: string) {
|
||||
let isReference = false
|
||||
let isInline = false
|
||||
|
||||
for (let option of segment(selector.slice(6) /* '@theme'.length */, ' ')) {
|
||||
if (option === 'reference') {
|
||||
isReference = true
|
||||
} else if (option === 'inline') {
|
||||
isInline = true
|
||||
}
|
||||
}
|
||||
|
||||
return { isReference, isInline }
|
||||
}
|
||||
|
||||
export function compile(
|
||||
css: string,
|
||||
{ loadPlugin = throwOnPlugin }: CompileOptions = {},
|
||||
@ -152,28 +167,32 @@ export function compile(
|
||||
}
|
||||
}
|
||||
|
||||
// Drop instances of `@media reference`
|
||||
// Drop instances of `@media theme(…)`
|
||||
//
|
||||
// We support `@import "tailwindcss/theme" reference` as a way to import an external theme file
|
||||
// as a reference, which becomes `@media reference { … }` when the `@import` is processed.
|
||||
if (node.selector === '@media reference') {
|
||||
// We support `@import "tailwindcss/theme" theme(reference)` as a way to
|
||||
// import an external theme file as a reference, which becomes `@media
|
||||
// theme(reference) { … }` when the `@import` is processed.
|
||||
if (node.selector.startsWith('@media theme(')) {
|
||||
let themeParams = node.selector.slice(13, -1)
|
||||
|
||||
walk(node.nodes, (child) => {
|
||||
if (child.kind !== 'rule') {
|
||||
throw new Error(
|
||||
'Files imported with `@import "…" reference` must only contain `@theme` blocks.',
|
||||
'Files imported with `@import "…" theme(…)` must only contain `@theme` blocks.',
|
||||
)
|
||||
}
|
||||
if (child.selector === '@theme') {
|
||||
child.selector = '@theme reference'
|
||||
child.selector = '@theme ' + themeParams
|
||||
return WalkAction.Skip
|
||||
}
|
||||
})
|
||||
replaceWith(node.nodes)
|
||||
return WalkAction.Skip
|
||||
}
|
||||
|
||||
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
|
||||
if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return
|
||||
|
||||
let isReference = node.selector === '@theme reference'
|
||||
let { isReference, isInline } = parseThemeOptions(node.selector)
|
||||
|
||||
// Record all custom properties in the `@theme` declaration
|
||||
walk(node.nodes, (child, { replaceWith }) => {
|
||||
@ -187,7 +206,7 @@ export function compile(
|
||||
|
||||
if (child.kind === 'comment') return
|
||||
if (child.kind === 'declaration' && child.property.startsWith('--')) {
|
||||
theme.add(child.property, child.value, isReference)
|
||||
theme.add(child.property, child.value, { isReference, isInline })
|
||||
return
|
||||
}
|
||||
|
||||
@ -395,15 +414,15 @@ export function __unstable__loadDesignSystem(css: string) {
|
||||
|
||||
walk(ast, (node) => {
|
||||
if (node.kind !== 'rule') return
|
||||
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
|
||||
let isReference = node.selector === '@theme reference'
|
||||
if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return
|
||||
let { isReference, isInline } = parseThemeOptions(node.selector)
|
||||
|
||||
// Record all custom properties in the `@theme` declaration
|
||||
walk([node], (node) => {
|
||||
if (node.kind !== 'declaration') return
|
||||
if (!node.property.startsWith('--')) return
|
||||
|
||||
theme.add(node.property, node.value, isReference)
|
||||
theme.add(node.property, node.value, { isReference, isInline })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { escape } from './utils/escape'
|
||||
|
||||
export class Theme {
|
||||
constructor(private values = new Map<string, { value: string; isReference: boolean }>()) {}
|
||||
constructor(
|
||||
private values = new Map<string, { value: string; isReference: boolean; isInline: boolean }>(),
|
||||
) {}
|
||||
|
||||
add(key: string, value: string, isReference = false): void {
|
||||
add(key: string, value: string, { isReference = false, isInline = false } = {}): void {
|
||||
if (key.endsWith('-*')) {
|
||||
if (value !== 'initial') {
|
||||
throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``)
|
||||
@ -18,7 +20,7 @@ export class Theme {
|
||||
if (value === 'initial') {
|
||||
this.values.delete(key)
|
||||
} else {
|
||||
this.values.set(key, { value, isReference })
|
||||
this.values.set(key, { value, isReference, isInline })
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,6 +93,12 @@ export class Theme {
|
||||
|
||||
if (!themeKey) return null
|
||||
|
||||
let value = this.values.get(themeKey)!
|
||||
|
||||
if (value.isInline) {
|
||||
return value.value
|
||||
}
|
||||
|
||||
return this.#var(themeKey)
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user