Skip to content

Commit 8d03db8

Browse files
RobinMalfaitphilipp-spiessthecrypticacereinink
authored
Implement --spacing(…), --alpha(…) and --theme(…) CSS functions (#15572)
This PR implements new CSS functions that you can use in your CSS (or even in arbitrary value position). For starters, we renamed the `theme(…)` function to `--theme(…)`. The legacy `theme(…)` function is still available for backwards compatibility reasons, but this allows us to be future proof since `--foo(…)` is the syntax the CSS spec recommends. See: https://drafts.csswg.org/css-mixins/ In addition, this PR implements a new `--spacing(…)` function, this allows you to write: ```css @import "tailwindcss"; @theme { --spacing: 0.25rem; } .foo { margin: --spacing(4): } ``` This is syntax sugar over: ```css @import "tailwindcss"; @theme { --spacing: 0.25rem; } .foo { margin: calc(var(--spacing) * 4); } ``` If your `@theme` uses the `inline` keyword, we will also make sure to inline the value: ```css @import "tailwindcss"; @theme inline { --spacing: 0.25rem; } .foo { margin: --spacing(4): } ``` Boils down to: ```css @import "tailwindcss"; @theme { --spacing: 0.25rem; } .foo { margin: calc(0.25rem * 4); /* And will be optimised to just 1rem */ } ``` --- Another new function function we added is the `--alpha(…)` function that requires a value, and a number / percentage value. This allows you to apply an alpha value to any color, but with a much shorter syntax: ```css @import "tailwindcss"; .foo { color: --alpha(var(--color-red-500), 0.5); } ``` This is syntax sugar over: ```css @import "tailwindcss"; .foo { color: color-mix(in oklab, var(--color-red-500) 50%, transparent); } ``` --------- Co-authored-by: Philipp Spiess <[email protected]> Co-authored-by: Jordan Pittman <[email protected]> Co-authored-by: Jonathan Reinink <[email protected]>
1 parent ee3add9 commit 8d03db8

File tree

8 files changed

+312
-102
lines changed

8 files changed

+312
-102
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Add `@tailwindcss/browser` package to run Tailwind CSS in the browser ([#15558](https://github.com/tailwindlabs/tailwindcss/pull/15558))
1313
- Add `@reference "…"` API as a replacement for the previous `@import "…" reference` option ([#15565](https://github.com/tailwindlabs/tailwindcss/pull/15565))
1414
- Add functional utility syntax ([#15455](https://github.com/tailwindlabs/tailwindcss/pull/15455))
15+
- Add new `--spacing(…)`, `--alpha(…)`, and `--theme(…)` CSS functions ([#15572](https://github.com/tailwindlabs/tailwindcss/pull/15572))
1516

1617
### Fixed
1718

packages/tailwindcss/src/compat/apply-compat-hooks.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,29 @@ function upgradeToFullPluginSupport({
253253
userConfig,
254254
)
255255

256+
// Replace `resolveThemeValue` with a version that is backwards compatible
257+
// with dot-notation but also aware of any JS theme configurations registered
258+
// by plugins or JS config files. This is significantly slower than just
259+
// upgrading dot-notation keys so we only use this version if plugins or
260+
// config files are actually being used. In the future we may want to optimize
261+
// this further by only doing this if plugins or config files _actually_
262+
// registered JS config objects.
263+
designSystem.resolveThemeValue = function resolveThemeValue(path: string, defaultValue?: string) {
264+
let resolvedValue = pluginApi.theme(path, defaultValue)
265+
266+
if (Array.isArray(resolvedValue) && resolvedValue.length === 2) {
267+
// When a tuple is returned, return the first element
268+
return resolvedValue[0]
269+
} else if (Array.isArray(resolvedValue)) {
270+
// Arrays get serialized into a comma-separated lists
271+
return resolvedValue.join(', ')
272+
} else if (typeof resolvedValue === 'string') {
273+
// Otherwise only allow string values here, objects (and namespace maps)
274+
// are treated as non-resolved values for the CSS `theme()` function.
275+
return resolvedValue
276+
}
277+
}
278+
256279
let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig, {
257280
set current(value: number) {
258281
features |= value
@@ -319,29 +342,6 @@ function upgradeToFullPluginSupport({
319342
designSystem.invalidCandidates.add(candidate)
320343
}
321344

322-
// Replace `resolveThemeValue` with a version that is backwards compatible
323-
// with dot-notation but also aware of any JS theme configurations registered
324-
// by plugins or JS config files. This is significantly slower than just
325-
// upgrading dot-notation keys so we only use this version if plugins or
326-
// config files are actually being used. In the future we may want to optimize
327-
// this further by only doing this if plugins or config files _actually_
328-
// registered JS config objects.
329-
designSystem.resolveThemeValue = function resolveThemeValue(path: string, defaultValue?: string) {
330-
let resolvedValue = pluginApi.theme(path, defaultValue)
331-
332-
if (Array.isArray(resolvedValue) && resolvedValue.length === 2) {
333-
// When a tuple is returned, return the first element
334-
return resolvedValue[0]
335-
} else if (Array.isArray(resolvedValue)) {
336-
// Arrays get serialized into a comma-separated lists
337-
return resolvedValue.join(', ')
338-
} else if (typeof resolvedValue === 'string') {
339-
// Otherwise only allow string values here, objects (and namespace maps)
340-
// are treated as non-resolved values for the CSS `theme()` function.
341-
return resolvedValue
342-
}
343-
}
344-
345345
for (let file of resolvedConfig.content.files) {
346346
if ('raw' in file) {
347347
throw new Error(

packages/tailwindcss/src/compat/plugin-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export function buildPluginApi(
9595
let api: PluginAPI = {
9696
addBase(css) {
9797
let baseNodes = objectToAst(css)
98-
featuresRef.current |= substituteFunctions(baseNodes, api.theme)
98+
featuresRef.current |= substituteFunctions(baseNodes, designSystem)
9999
ast.push(atRule('@layer', 'base', baseNodes))
100100
},
101101

packages/tailwindcss/src/compile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function compileCandidates(
6262
try {
6363
substituteFunctions(
6464
rules.map(({ node }) => node),
65-
designSystem.resolveThemeValue,
65+
designSystem,
6666
)
6767
} catch (err) {
6868
// If substitution fails then the candidate likely contains a call to

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

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,168 @@ import { compileCss, optimizeCss } from './test-utils/run'
77

88
const css = String.raw
99

10-
describe('theme function', () => {
10+
describe('--alpha(…)', () => {
11+
test('--alpha(…)', async () => {
12+
expect(
13+
await compileCss(css`
14+
.foo {
15+
margin: --alpha(red, 50%);
16+
}
17+
`),
18+
).toMatchInlineSnapshot(`
19+
".foo {
20+
margin: oklab(62.7955% .22486 .12584 / .5);
21+
}"
22+
`)
23+
})
24+
25+
test('--alpha(…) errors when no arguments are used', async () => {
26+
expect(() =>
27+
compileCss(css`
28+
.foo {
29+
margin: --alpha();
30+
}
31+
`),
32+
).rejects.toThrowErrorMatchingInlineSnapshot(
33+
`[Error: The --alpha(…) function requires two arguments, e.g.: \`--alpha(var(--my-color), 50%)\`]`,
34+
)
35+
})
36+
37+
test('--alpha(…) errors when alpha value is missing', async () => {
38+
expect(() =>
39+
compileCss(css`
40+
.foo {
41+
margin: --alpha(red);
42+
}
43+
`),
44+
).rejects.toThrowErrorMatchingInlineSnapshot(
45+
`[Error: The --alpha(…) function requires two arguments, e.g.: \`--alpha(red, 50%)\`]`,
46+
)
47+
})
48+
49+
test('--alpha(…) errors multiple arguments are used', async () => {
50+
expect(() =>
51+
compileCss(css`
52+
.foo {
53+
margin: --alpha(red, 50%, blue);
54+
}
55+
`),
56+
).rejects.toThrowErrorMatchingInlineSnapshot(
57+
`[Error: The --alpha(…) function only accepts two arguments, e.g.: \`--alpha(red, 50%)\`]`,
58+
)
59+
})
60+
})
61+
62+
describe('--spacing(…)', () => {
63+
test('--spacing(…)', async () => {
64+
expect(
65+
await compileCss(css`
66+
@theme {
67+
--spacing: 0.25rem;
68+
}
69+
70+
.foo {
71+
margin: --spacing(4);
72+
}
73+
`),
74+
).toMatchInlineSnapshot(`
75+
":root {
76+
--spacing: .25rem;
77+
}
78+
79+
.foo {
80+
margin: calc(var(--spacing) * 4);
81+
}"
82+
`)
83+
})
84+
85+
test('--spacing(…) with inline `@theme` value', async () => {
86+
expect(
87+
await compileCss(css`
88+
@theme inline {
89+
--spacing: 0.25rem;
90+
}
91+
92+
.foo {
93+
margin: --spacing(4);
94+
}
95+
`),
96+
).toMatchInlineSnapshot(`
97+
":root {
98+
--spacing: .25rem;
99+
}
100+
101+
.foo {
102+
margin: 1rem;
103+
}"
104+
`)
105+
})
106+
107+
test('--spacing(…) relies on `--spacing` to be defined', async () => {
108+
expect(() =>
109+
compileCss(css`
110+
.foo {
111+
margin: --spacing(4);
112+
}
113+
`),
114+
).rejects.toThrowErrorMatchingInlineSnapshot(
115+
`[Error: The --spacing(…) function requires that the \`--spacing\` theme variable exists, but it was not found.]`,
116+
)
117+
})
118+
119+
test('--spacing(…) requires a single value', async () => {
120+
expect(() =>
121+
compileCss(css`
122+
@theme {
123+
--spacing: 0.25rem;
124+
}
125+
126+
.foo {
127+
margin: --spacing();
128+
}
129+
`),
130+
).rejects.toThrowErrorMatchingInlineSnapshot(
131+
`[Error: The --spacing(…) function requires an argument, but received none.]`,
132+
)
133+
})
134+
135+
test('--spacing(…) does not have multiple arguments', async () => {
136+
expect(() =>
137+
compileCss(css`
138+
.foo {
139+
margin: --spacing(4, 5, 6);
140+
}
141+
`),
142+
).rejects.toThrowErrorMatchingInlineSnapshot(
143+
`[Error: The --spacing(…) function only accepts a single argument, but received 3.]`,
144+
)
145+
})
146+
})
147+
148+
describe('--theme(…)', () => {
149+
test('theme(--color-red-500)', async () => {
150+
expect(
151+
await compileCss(css`
152+
@theme {
153+
--color-red-500: #f00;
154+
}
155+
.red {
156+
color: --theme(--color-red-500);
157+
}
158+
`),
159+
).toMatchInlineSnapshot(`
160+
":root {
161+
--color-red-500: red;
162+
}
163+
164+
.red {
165+
color: red;
166+
}"
167+
`)
168+
})
169+
})
170+
171+
describe('theme(…)', () => {
11172
describe('in declaration values', () => {
12173
describe('without fallback values', () => {
13174
test('theme(colors.red.500)', async () => {

0 commit comments

Comments
 (0)