From 04588b1e8f3dbc856d8a3fad97c460bee689dbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B5=AD?= Date: Thu, 2 Jul 2026 22:20:14 +0900 Subject: [PATCH] Fix theme() in JS plugins returning unresolved object instead of DEFAULT value (#20299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary When a CSS theme key defined via `@theme` (or a JS config's `theme` object) shares a dash-separated prefix with a sibling key — e.g. `--color-foo` and `--color-foo-bar` — calling `theme('colors.foo')` from inside a JS plugin (`addUtilities`, `addComponents`, etc.) does not resolve to the `foo` value. Instead it returns an internal disambiguation object shaped like `{ DEFAULT: 'red', bar: 'blue', __CSS_VALUES__: {...} }`, because there's no way to tell from CSS custom property names alone whether `foo-bar` is a sibling key or a nested sub-key of `foo`. This same ambiguity was already fixed for the CSS-embedded `theme()` function in #19097 (which unwraps to the `DEFAULT` key when present), and the changelog entry for that PR states it fixes this "in JS configs **and plugins**" — but the fix only touched `apply-compat-hooks.ts`'s `resolveThemeValue`, not `createThemeFn`'s `theme` function that's exposed directly to plugins in `plugin-functions.ts`. This PR closes that gap by applying the same DEFAULT-unwrapping there. Without this fix, passing the raw object into `addUtilities` (a very natural thing to do, since a plugin author expects a string) produces broken CSS — the reserved `DEFAULT` key gets mangled into a garbage property name (`-d-e-f-a-u-l-t`) by the kebab-case conversion, and the internal `__CSS_VALUES__` bookkeeping leaks into the generated stylesheet. ### Minimal reproduction ```js // tailwind.config.js (registered via @config, or any @plugin-registered plugin) const plugin = require('tailwindcss/plugin') module.exports = { plugins: [ plugin(function ({ addUtilities, theme }) { addUtilities({ '.example-foo': { color: theme('colors.foo') }, }) }), ], } ``` ```css @import "tailwindcss"; @config "./tailwind.config.js"; @theme { --color-foo: red; --color-foo-bar: blue; } ``` **Before:** ```css .example-foo color { -d-e-f-a-u-l-t: red; bar: blue; } .example-foo color __CSS_VALUES__ { -d-e-f-a-u-l-t: 0; bar: 0; } ``` **After:** ```css .example-foo { color: red; } ``` ## Test plan - Added a regression test in `packages/tailwindcss/src/compat/plugin-api.test.ts` ("theme() resolves the DEFAULT value when a bare CSS theme key shares a prefix with a sibling key") - Verified via `pnpm --filter tailwindcss exec vitest run` that this test fails with the exact broken output shown above when the fix is reverted, and passes once it's applied - Ran the full `tailwindcss` package test suite (`pnpm --filter tailwindcss exec vitest run`) — 4684 tests passing, no regressions - Verified formatting on the changed files with `npx prettier --check` --------- Co-authored-by: Robin Malfait --- CHANGELOG.md | 1 + .../tailwindcss/src/compat/plugin-api.test.ts | 40 +++++++++++++++++++ .../src/compat/plugin-functions.ts | 17 ++++++++ 3 files changed, 58 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6866fb88..b4058a772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow `@tailwindcss/cli` in `--watch` mode to use polling with `--poll` when filesystem events are unreliable or unavailable ([#20297](https://github.com/tailwindlabs/tailwindcss/pull/20297)) - Canonicalization: match arbitrary hex colors against theme colors case-insensitively (e.g. `bg-[#fff]` and `bg-[#FFF]` → `bg-white`) ([#20298](https://github.com/tailwindlabs/tailwindcss/pull/20298)) - Prevent Preflight from overriding Firefox's native `iframe:focus-visible` outline styles ([#20292](https://github.com/tailwindlabs/tailwindcss/pull/20292)) +- Prevent `theme('colors.foo')` in JS plugins from returning an internal disambiguation object when a CSS theme key shares a prefix with a sibling key like `--color-foo-bar` ([#20299](https://github.com/tailwindlabs/tailwindcss/pull/20299)) ## [4.3.2] - 2026-06-26 diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index e3ea3fb07..2ecdbe9a3 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -635,6 +635,46 @@ describe('theme', async () => { }) }) + test('theme() resolves the DEFAULT value when a bare CSS theme key shares a prefix with a sibling key', async () => { + expect( + await run( + ['example-foo', 'example-foo-bar'], + css` + @tailwind utilities; + @theme { + --color-foo: red; + --color-foo-bar: blue; + } + @plugin "my-plugin"; + `, + { + loadModule: async (_id, base) => { + return { + path: '', + base, + module: plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.example-foo': { color: theme('colors.foo') }, + '.example-foo-bar': { color: theme('colors.foo-bar') }, + }) + }), + } + }, + }, + ), + ).toMatchInlineSnapshot(` + " + .example-foo { + color: red; + } + + .example-foo-bar { + color: #00f; + } + " + `) + }) + test('all necessary theme keys support bare values', async () => { expect( await run( diff --git a/packages/tailwindcss/src/compat/plugin-functions.ts b/packages/tailwindcss/src/compat/plugin-functions.ts index 40b8c93e8..ee795c40f 100644 --- a/packages/tailwindcss/src/compat/plugin-functions.ts +++ b/packages/tailwindcss/src/compat/plugin-functions.ts @@ -100,6 +100,23 @@ export function createThemeFn( return [base, extra] } + // If `--color-foo` and `--color-foo-bar` are both defined, `colors.foo` + // produces a synthetic object: + // + // ```ts + // { DEFAULT: 'red', bar: 'blue', __CSS_VALUES__: { DEFAULT: 0, bar: 0 } } + // ``` + // + // Prefer `DEFAULT` instead of exposing the object to plugin code. + if ( + cssValue !== null && + typeof cssValue === 'object' && + !Array.isArray(cssValue) && + 'DEFAULT' in cssValue + ) { + return cssValue.DEFAULT + } + // Values from CSS take precedence over values from the config return cssValue ?? configValue })()