diff --git a/CHANGELOG.md b/CHANGELOG.md index e6866fb886c0..b4058a7729bf 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 e3ea3fb07492..2ecdbe9a3202 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 40b8c93e89f1..ee795c40f16d 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 })()