Skip to content

Commit 0c14df1

Browse files
authored
Fix resolving colors via theme(…) in compat mode with nested objects (#19097)
This PR fixes an issue when loading (nested) colors from a config file and later referencing it via the `theme(…)` function in CSS. Given a config like this: ```js module.exports = { theme: { colors: { foo: 'var(--foo-foo)', 'foo-bar': 'var(--foo-foo-bar)', }, }, } ``` We internally map this into the design system. The issue here is that the `foo` and `foo-bar` are overlapping and it behaves more like this: ```js { foo: { DEFAULT: 'var(--foo-foo)', bar: 'var(--foo-foo-bar)' }, } ``` So while we can easily resolve `colors.foo-bar`, the `colors.foo` would result in the object with a `DEFAULT` key. This PR solves that by using the `DEFAULT` key if we end up with an object that has it. If you end up resolving an object (`theme(colors)`) then the behavior is unchanged. ## Test plan 1. Added a test based on the config in the issue (which failed before this fix). 2. Also simplified the test case after identifying the problem (with the `DEFAULT` key). Fixes: #19091
1 parent 561983d commit 0c14df1

File tree

4 files changed

+93
-10
lines changed

4 files changed

+93
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
- Fix Safari devtools rendering issue due to `color-mix` fallback ([#19069](https://github.com/tailwindlabs/tailwindcss/pull/19069))
1717
- Suppress Lightning CSS warnings about `:deep`, `:slotted` and `:global` ([#19094](https://github.com/tailwindlabs/tailwindcss/pull/19094))
18+
- Fix resolving theme keys when starting with the name of another theme key in JS configs and plugins ([#19097](https://github.com/tailwindlabs/tailwindcss/pull/19097))
1819

1920
## [4.1.14] - 2025-10-01
2021

packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function migrateMediaScreen({
1616
if (!designSystem || !userConfig) return
1717

1818
let { resolvedConfig } = resolveConfig(designSystem, [
19-
{ base: '', config: userConfig, reference: false },
19+
{ base: '', config: userConfig, reference: false, src: undefined },
2020
])
2121
let screens = resolvedConfig?.theme?.screens || {}
2222

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -309,15 +309,29 @@ function upgradeToFullPluginSupport({
309309

310310
let resolvedValue = sharedPluginApi.theme(path, undefined)
311311

312+
// When a tuple is returned, return the first element
312313
if (Array.isArray(resolvedValue) && resolvedValue.length === 2) {
313-
// When a tuple is returned, return the first element
314314
return resolvedValue[0]
315-
} else if (Array.isArray(resolvedValue)) {
316-
// Arrays get serialized into a comma-separated lists
315+
}
316+
317+
// Arrays get serialized into a comma-separated lists
318+
else if (Array.isArray(resolvedValue)) {
317319
return resolvedValue.join(', ')
318-
} else if (typeof resolvedValue === 'string') {
319-
// Otherwise only allow string values here, objects (and namespace maps)
320-
// are treated as non-resolved values for the CSS `theme()` function.
320+
}
321+
322+
// If we're dealing with an object that has the `DEFAULT` key, return the
323+
// default value
324+
else if (
325+
typeof resolvedValue === 'object' &&
326+
resolvedValue !== null &&
327+
'DEFAULT' in resolvedValue
328+
) {
329+
return resolvedValue.DEFAULT
330+
}
331+
332+
// Otherwise only allow string values here, objects (and namespace maps)
333+
// are treated as non-resolved values for the CSS `theme()` function.
334+
else if (typeof resolvedValue === 'string') {
321335
return resolvedValue
322336
}
323337
}

packages/tailwindcss/src/compat/config.test.ts

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ test('Config files can add content', async () => {
1212
`
1313

1414
let compiler = await compile(input, {
15-
loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root' }),
15+
loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root', path: '' }),
1616
})
1717

1818
expect(compiler.sources).toEqual([{ base: '/root', pattern: './file.txt', negated: false }])
@@ -25,7 +25,7 @@ test('Config files can change dark mode (media)', async () => {
2525
`
2626

2727
let compiler = await compile(input, {
28-
loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root' }),
28+
loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root', path: '' }),
2929
})
3030

3131
expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(`
@@ -45,7 +45,7 @@ test('Config files can change dark mode (selector)', async () => {
4545
`
4646

4747
let compiler = await compile(input, {
48-
loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root' }),
48+
loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root', path: '' }),
4949
})
5050

5151
expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(`
@@ -68,6 +68,7 @@ test('Config files can change dark mode (variant)', async () => {
6868
loadModule: async () => ({
6969
module: { darkMode: ['variant', '&:where(:not(.light))'] },
7070
base: '/root',
71+
path: '',
7172
}),
7273
})
7374

@@ -101,6 +102,7 @@ test('Config files can add plugins', async () => {
101102
],
102103
},
103104
base: '/root',
105+
path: '',
104106
}),
105107
})
106108

@@ -128,6 +130,7 @@ test('Plugins loaded from config files can contribute to the config', async () =
128130
],
129131
},
130132
base: '/root',
133+
path: '',
131134
}),
132135
})
133136

@@ -157,6 +160,7 @@ test('Config file presets can contribute to the config', async () => {
157160
],
158161
},
159162
base: '/root',
163+
path: '',
160164
}),
161165
})
162166

@@ -198,6 +202,7 @@ test('Config files can affect the theme', async () => {
198202
],
199203
},
200204
base: '/root',
205+
path: '',
201206
}),
202207
})
203208

@@ -212,6 +217,48 @@ test('Config files can affect the theme', async () => {
212217
`)
213218
})
214219

220+
// https://github.com/tailwindlabs/tailwindcss/issues/19091
221+
test('Accessing a default color if a sub-color exists via CSS should work as expected', async () => {
222+
let input = css`
223+
@tailwind utilities;
224+
@config "./config.js";
225+
226+
.example {
227+
color: theme('colors.foo-bar');
228+
border-color: theme('colors.foo');
229+
}
230+
`
231+
232+
let compiler = await compile(input, {
233+
loadModule: async () => ({
234+
module: {
235+
theme: {
236+
// Internally this object gets converted to something like:
237+
// ```
238+
// {
239+
// foo: { DEFAULT: 'var(--foo-foo)', bar: 'var(--foo-foo-bar)' },
240+
// }
241+
// ```
242+
colors: {
243+
foo: 'var(--foo-foo)',
244+
'foo-bar': 'var(--foo-foo-bar)',
245+
},
246+
},
247+
},
248+
base: '/root',
249+
path: '',
250+
}),
251+
})
252+
253+
expect(compiler.build([])).toMatchInlineSnapshot(`
254+
".example {
255+
color: var(--foo-foo-bar);
256+
border-color: var(--foo-foo);
257+
}
258+
"
259+
`)
260+
})
261+
215262
test('Variants in CSS overwrite variants from plugins', async () => {
216263
let input = css`
217264
@tailwind utilities;
@@ -231,6 +278,7 @@ test('Variants in CSS overwrite variants from plugins', async () => {
231278
],
232279
},
233280
base: '/root',
281+
path: '',
234282
}),
235283
})
236284

@@ -317,6 +365,7 @@ describe('theme callbacks', () => {
317365
],
318366
} satisfies Config,
319367
base: '/root',
368+
path: '',
320369
}),
321370
})
322371

@@ -391,6 +440,7 @@ describe('theme overrides order', () => {
391440
},
392441
},
393442
base: '/root',
443+
path: '',
394444
}),
395445
})
396446

@@ -442,6 +492,7 @@ describe('theme overrides order', () => {
442492
},
443493
} satisfies Config,
444494
base: '/root',
495+
path: '',
445496
}
446497
} else {
447498
return {
@@ -460,6 +511,7 @@ describe('theme overrides order', () => {
460511
)
461512
}),
462513
base: '/root',
514+
path: '',
463515
}
464516
}
465517
},
@@ -562,6 +614,7 @@ describe('default font family compatibility', () => {
562614
},
563615
},
564616
base: '/root',
617+
path: '',
565618
}),
566619
})
567620

@@ -596,6 +649,7 @@ describe('default font family compatibility', () => {
596649
},
597650
},
598651
base: '/root',
652+
path: '',
599653
}),
600654
})
601655

@@ -631,6 +685,7 @@ describe('default font family compatibility', () => {
631685
},
632686
},
633687
base: '/root',
688+
path: '',
634689
}),
635690
})
636691

@@ -669,6 +724,7 @@ describe('default font family compatibility', () => {
669724
},
670725
},
671726
base: '/root',
727+
path: '',
672728
}),
673729
})
674730

@@ -708,6 +764,7 @@ describe('default font family compatibility', () => {
708764
},
709765
},
710766
base: '/root',
767+
path: '',
711768
}),
712769
})
713770

@@ -745,6 +802,7 @@ describe('default font family compatibility', () => {
745802
},
746803
},
747804
base: '/root',
805+
path: '',
748806
}),
749807
})
750808

@@ -779,6 +837,7 @@ describe('default font family compatibility', () => {
779837
},
780838
},
781839
base: '/root',
840+
path: '',
782841
}),
783842
})
784843

@@ -806,6 +865,7 @@ describe('default font family compatibility', () => {
806865
},
807866
},
808867
base: '/root',
868+
path: '',
809869
}),
810870
})
811871

@@ -840,6 +900,7 @@ describe('default font family compatibility', () => {
840900
},
841901
},
842902
base: '/root',
903+
path: '',
843904
}),
844905
})
845906

@@ -875,6 +936,7 @@ describe('default font family compatibility', () => {
875936
},
876937
},
877938
base: '/root',
939+
path: '',
878940
}),
879941
})
880942

@@ -913,6 +975,7 @@ describe('default font family compatibility', () => {
913975
},
914976
},
915977
base: '/root',
978+
path: '',
916979
}),
917980
})
918981

@@ -952,6 +1015,7 @@ describe('default font family compatibility', () => {
9521015
},
9531016
},
9541017
base: '/root',
1018+
path: '',
9551019
}),
9561020
})
9571021

@@ -989,6 +1053,7 @@ describe('default font family compatibility', () => {
9891053
},
9901054
},
9911055
base: '/root',
1056+
path: '',
9921057
}),
9931058
})
9941059

@@ -1021,6 +1086,7 @@ test('creates variants for `data`, `supports`, and `aria` theme options at the s
10211086
},
10221087
},
10231088
base: '/root',
1089+
path: '',
10241090
}),
10251091
})
10261092

@@ -1113,6 +1179,7 @@ test('merges css breakpoints with js config screens', async () => {
11131179
},
11141180
},
11151181
base: '/root',
1182+
path: '',
11161183
}),
11171184
})
11181185

@@ -1596,6 +1663,7 @@ test('handles setting theme keys to null', async () => {
15961663
},
15971664
},
15981665
base: '/root',
1666+
path: '',
15991667
}
16001668
},
16011669
},

0 commit comments

Comments
 (0)