Skip to content

Commit 8f8803d

Browse files
Move opacity modifier support into plugin theme() function (#14348)
This PR moves support for opacity modifies from the CSS `theme()` function into the plugin `theme()` implementation, this will allow plugins to use this, too: ```ts let plugin = plugin(function ({ addUtilities, theme }) { addUtilities({ '.percentage': { color: theme('colors.red.500 / 50%'), }, '.fraction': { color: theme('colors.red.500 / 0.5'), }, '.variable': { color: theme('colors.red.500 / var(--opacity)'), }, }) }) } ``` There's a small behavioral change for the CSS `theme()` function. Since tuples are resolved by default for the CSS `theme()` function only, these will no longer have opacity applied to their first values. This is probably fine given the reduced complexity as I don't expect the first values of tuples to be colors and the fix would mean we would have to parse the modifier in different places.
1 parent d9558bb commit 8f8803d

File tree

8 files changed

+93
-41
lines changed

8 files changed

+93
-41
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 opacity modifier support to the `theme()` function in plugins ([#14348](https://github.com/tailwindlabs/tailwindcss/pull/14348))
1113

1214
## [4.0.0-alpha.22] - 2024-09-04
1315

packages/tailwindcss/src/compat/config/resolve-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { DesignSystem } from '../../design-system'
22
import type { PluginWithConfig } from '../../plugin-api'
3-
import { createThemeFn } from '../../theme-fn'
3+
import { createThemeFn } from '../plugin-functions'
44
import { deepMerge, isPlainObject } from './deep-merge'
55
import {
66
type ResolvedConfig,

packages/tailwindcss/src/theme-fn.ts renamed to packages/tailwindcss/src/compat/plugin-functions.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,51 @@
1-
import { deepMerge } from './compat/config/deep-merge'
2-
import type { UserConfig } from './compat/config/types'
3-
import type { DesignSystem } from './design-system'
4-
import type { Theme, ThemeKey } from './theme'
5-
import { DefaultMap } from './utils/default-map'
6-
import { toKeyPath } from './utils/to-key-path'
1+
import type { DesignSystem } from '../design-system'
2+
import type { Theme, ThemeKey } from '../theme'
3+
import { withAlpha } from '../utilities'
4+
import { DefaultMap } from '../utils/default-map'
5+
import { toKeyPath } from '../utils/to-key-path'
6+
import { deepMerge } from './config/deep-merge'
7+
import type { UserConfig } from './config/types'
78

89
export function createThemeFn(
910
designSystem: DesignSystem,
1011
configTheme: () => UserConfig['theme'],
1112
resolveValue: (value: any) => any,
1213
) {
1314
return function theme(path: string, defaultValue?: any) {
14-
let keypath = toKeyPath(path)
15-
let cssValue = readFromCss(designSystem.theme, keypath)
16-
17-
if (typeof cssValue !== 'object') {
18-
return cssValue
15+
// Extract an eventual modifier from the path. e.g.:
16+
// - "colors.red.500 / 50%" -> "50%"
17+
// - "foo/bar/baz/50%" -> "50%"
18+
let lastSlash = path.lastIndexOf('/')
19+
let modifier: string | null = null
20+
if (lastSlash !== -1) {
21+
modifier = path.slice(lastSlash + 1).trim()
22+
path = path.slice(0, lastSlash).trim()
1923
}
2024

21-
let configValue = resolveValue(get(configTheme() ?? {}, keypath) ?? null)
25+
let resolvedValue = (() => {
26+
let keypath = toKeyPath(path)
27+
let cssValue = readFromCss(designSystem.theme, keypath)
28+
29+
if (typeof cssValue !== 'object') {
30+
return cssValue
31+
}
32+
33+
let configValue = resolveValue(get(configTheme() ?? {}, keypath) ?? null)
34+
35+
if (configValue !== null && typeof configValue === 'object' && !Array.isArray(configValue)) {
36+
return deepMerge({}, [configValue, cssValue], (_, b) => b)
37+
}
38+
39+
// Values from CSS take precedence over values from the config
40+
return cssValue ?? configValue
41+
})()
2242

23-
if (configValue !== null && typeof configValue === 'object' && !Array.isArray(configValue)) {
24-
return deepMerge({}, [configValue, cssValue], (_, b) => b)
43+
// Apply the opacity modifier if present
44+
if (modifier && typeof resolvedValue === 'string') {
45+
resolvedValue = withAlpha(resolvedValue, modifier)
2546
}
2647

27-
// Values from CSS take precedence over values from the config
28-
return cssValue ?? configValue ?? defaultValue
48+
return resolvedValue ?? defaultValue
2949
}
3050
}
3151

packages/tailwindcss/src/functions.ts renamed to packages/tailwindcss/src/css-functions.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { walk, type AstNode } from './ast'
22
import type { PluginAPI } from './plugin-api'
3-
import { withAlpha } from './utilities'
43
import * as ValueParser from './value-parser'
54
import { type ValueAstNode } from './value-parser'
65

@@ -77,27 +76,19 @@ function cssThemeFn(
7776
path: string,
7877
fallbackValues: ValueAstNode[],
7978
): ValueAstNode[] {
80-
let modifier: string | null = null
81-
// Extract an eventual modifier from the path. e.g.:
82-
// - "colors.red.500 / 50%" -> "50%"
83-
// - "foo/bar/baz/50%" -> "50%"
84-
let lastSlash = path.lastIndexOf('/')
85-
if (lastSlash !== -1) {
86-
modifier = path.slice(lastSlash + 1).trim()
87-
path = path.slice(0, lastSlash).trim()
88-
}
89-
9079
let resolvedValue: string | null = null
9180
let themeValue = pluginApi.theme(path)
9281

93-
if (Array.isArray(themeValue) && themeValue.length === 2) {
82+
let isArray = Array.isArray(themeValue)
83+
if (isArray && themeValue.length === 2) {
9484
// When a tuple is returned, return the first element
9585
resolvedValue = themeValue[0]
96-
// We otherwise only ignore string values here, objects (and namespace maps)
97-
// are treated as non-resolved values for the CSS `theme()` function.
98-
} else if (Array.isArray(themeValue)) {
86+
} else if (isArray) {
87+
// Arrays get serialized into a comma-separated lists
9988
resolvedValue = themeValue.join(', ')
10089
} else if (typeof themeValue === 'string') {
90+
// Otherwise only allow string values here, objects (and namespace maps)
91+
// are treated as non-resolved values for the CSS `theme()` function.
10192
resolvedValue = themeValue
10293
}
10394

@@ -107,14 +98,10 @@ function cssThemeFn(
10798

10899
if (!resolvedValue) {
109100
throw new Error(
110-
`Could not resolve value for theme function: \`theme(${path}${modifier ? ` / ${modifier}` : ''})\`. Consider checking if the path is correct or provide a fallback value to silence this error.`,
101+
`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.`,
111102
)
112103
}
113104

114-
if (modifier) {
115-
resolvedValue = withAlpha(resolvedValue, modifier)
116-
}
117-
118105
// We need to parse the values recursively since this can resolve with another
119106
// `theme()` function definition.
120107
return ValueParser.parse(resolvedValue)

packages/tailwindcss/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { substituteAtApply } from './apply'
33
import { comment, decl, rule, toCss, walk, WalkAction, type Rule } from './ast'
44
import type { UserConfig } from './compat/config/types'
55
import { compileCandidates } from './compile'
6+
import { substituteFunctions, THEME_FUNCTION_INVOCATION } from './css-functions'
67
import * as CSS from './css-parser'
78
import { buildDesignSystem, type DesignSystem } from './design-system'
8-
import { substituteFunctions, THEME_FUNCTION_INVOCATION } from './functions'
99
import { registerPlugins, type CssPluginOptions, type Plugin } from './plugin-api'
1010
import { Theme } from './theme'
1111
import { segment } from './utils/segment'

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,49 @@ describe('theme', async () => {
208208
`)
209209
})
210210

211+
test('plugin theme can have opacity modifiers', async ({ expect }) => {
212+
let input = css`
213+
@tailwind utilities;
214+
@theme {
215+
--color-red-500: #ef4444;
216+
}
217+
@plugin "my-plugin";
218+
`
219+
220+
let compiler = await compile(input, {
221+
loadPlugin: async () => {
222+
return plugin(function ({ addUtilities, theme }) {
223+
addUtilities({
224+
'.percentage': {
225+
color: theme('colors.red.500 / 50%'),
226+
},
227+
'.fraction': {
228+
color: theme('colors.red.500 / 0.5'),
229+
},
230+
'.variable': {
231+
color: theme('colors.red.500 / var(--opacity)'),
232+
},
233+
})
234+
})
235+
},
236+
})
237+
238+
expect(compiler.build(['percentage', 'fraction', 'variable'])).toMatchInlineSnapshot(`
239+
".fraction {
240+
color: color-mix(in srgb, #ef4444 50%, transparent);
241+
}
242+
.percentage {
243+
color: color-mix(in srgb, #ef4444 50%, transparent);
244+
}
245+
.variable {
246+
color: color-mix(in srgb, #ef4444 calc(var(--opacity) * 100%), transparent);
247+
}
248+
:root {
249+
--color-red-500: #ef4444;
250+
}
251+
"
252+
`)
253+
})
211254
test('theme value functions are resolved correctly regardless of order', async ({ expect }) => {
212255
let input = css`
213256
@tailwind utilities;
@@ -354,7 +397,7 @@ describe('theme', async () => {
354397
`)
355398
})
356399

357-
test('CSS theme values are mreged with JS theme values', async ({ expect }) => {
400+
test('CSS theme values are merged with JS theme values', async ({ expect }) => {
358401
let input = css`
359402
@tailwind utilities;
360403
@plugin "my-plugin";

packages/tailwindcss/src/plugin-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { createCompatConfig } from './compat/config/create-compat-config'
66
import { resolveConfig, type ConfigFile } from './compat/config/resolve-config'
77
import type { ResolvedConfig, UserConfig } from './compat/config/types'
88
import { darkModePlugin } from './compat/dark-mode'
9+
import { createThemeFn } from './compat/plugin-functions'
910
import type { DesignSystem } from './design-system'
10-
import { createThemeFn } from './theme-fn'
1111
import { withAlpha, withNegative } from './utilities'
1212
import { inferDataType } from './utils/infer-data-type'
1313
import { segment } from './utils/segment'

0 commit comments

Comments
 (0)