Skip to content

Commit 56c3b68

Browse files
Put components into an extra layer when using the legacy JS API
1 parent d8c4df8 commit 56c3b68

File tree

5 files changed

+222
-56
lines changed

5 files changed

+222
-56
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Fix incorrectly replacing `_` with ` ` in arbitrary modifier shorthand `bg-red-500/(--my_opacity)` ([#17889](https://github.com/tailwindlabs/tailwindcss/pull/17889))
2121
- Upgrade: Bump dependencies in parallel and make the upgrade faster ([#17898](https://github.com/tailwindlabs/tailwindcss/pull/17898))
2222
- Don't scan `.log` files for classes by default ([#17906](https://github.com/tailwindlabs/tailwindcss/pull/17906))
23+
- Ensure that components added via the JS API are put into a layer so they can always be overwritten by utilities ([#17918](https://github.com/tailwindlabs/tailwindcss/pull/17918))
2324

2425
## [4.1.5] - 2025-04-30
2526

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

+86-33
Original file line numberDiff line numberDiff line change
@@ -4113,17 +4113,19 @@ describe('matchUtilities()', () => {
41134113
})
41144114

41154115
describe('addComponents()', () => {
4116-
test('is an alias for addUtilities', async () => {
4116+
test('is an alias for addUtilities that wraps the code in \`@layer components\`', async () => {
41174117
let compiled = await compile(
41184118
css`
41194119
@plugin "my-plugin";
4120-
@tailwind utilities;
4120+
@layer utilities {
4121+
@tailwind utilities;
4122+
}
41214123
`,
41224124
{
41234125
async loadModule(id, base) {
41244126
return {
41254127
base,
4126-
module: ({ addComponents }: PluginAPI) => {
4128+
module: ({ addComponents, addUtilities }: PluginAPI) => {
41274129
addComponents({
41284130
'.btn': {
41294131
padding: '.5rem 1rem',
@@ -4145,43 +4147,60 @@ describe('addComponents()', () => {
41454147
},
41464148
},
41474149
})
4150+
addUtilities({
4151+
'.btn-utility': {
4152+
padding: '.5rem 1rem',
4153+
borderRadius: '.25rem',
4154+
fontWeight: '600',
4155+
},
4156+
})
41484157
},
41494158
}
41504159
},
41514160
},
41524161
)
41534162

4154-
expect(optimizeCss(compiled.build(['btn', 'btn-blue', 'btn-red'])).trim())
4163+
expect(optimizeCss(compiled.build(['btn', 'btn-blue', 'btn-red', 'btn-utility'])).trim())
41554164
.toMatchInlineSnapshot(`
4156-
".btn {
4157-
border-radius: .25rem;
4158-
padding: .5rem 1rem;
4159-
font-weight: 600;
4160-
}
4165+
"@layer utilities {
4166+
@layer components {
4167+
.btn {
4168+
border-radius: .25rem;
4169+
padding: .5rem 1rem;
4170+
font-weight: 600;
4171+
}
41614172
4162-
.btn-blue {
4163-
color: #fff;
4164-
background-color: #3490dc;
4165-
}
4173+
.btn-blue {
4174+
color: #fff;
4175+
background-color: #3490dc;
4176+
}
41664177
4167-
.btn-blue:hover {
4168-
background-color: #2779bd;
4169-
}
4178+
.btn-blue:hover {
4179+
background-color: #2779bd;
4180+
}
41704181
4171-
.btn-red {
4172-
color: #fff;
4173-
background-color: #e3342f;
4174-
}
4182+
.btn-red {
4183+
color: #fff;
4184+
background-color: #e3342f;
4185+
}
41754186
4176-
.btn-red:hover {
4177-
background-color: #cc1f1a;
4187+
.btn-red:hover {
4188+
background-color: #cc1f1a;
4189+
}
4190+
}
4191+
4192+
.btn-utility {
4193+
border-radius: .25rem;
4194+
padding: .5rem 1rem;
4195+
font-weight: 600;
4196+
}
41784197
}"
41794198
`)
41804199
})
41814200
})
41824201

41834202
describe('matchComponents()', () => {
4184-
test('is an alias for matchUtilities', async () => {
4203+
test('is an alias for matchUtilities that wraps the code in \`@layer components\`', async () => {
41854204
let compiled = await compile(
41864205
css`
41874206
@plugin "my-plugin";
@@ -4191,10 +4210,22 @@ describe('matchComponents()', () => {
41914210
async loadModule(id, base) {
41924211
return {
41934212
base,
4194-
module: ({ matchComponents }: PluginAPI) => {
4213+
module: ({ matchComponents, matchUtilities }: PluginAPI) => {
41954214
matchComponents(
41964215
{
4197-
prose: (value) => ({ '--container-size': value }),
4216+
'prose-component': (value) => ({ '--container-size': value }),
4217+
},
4218+
{
4219+
values: {
4220+
DEFAULT: 'normal',
4221+
sm: 'sm',
4222+
lg: 'lg',
4223+
},
4224+
},
4225+
)
4226+
matchUtilities(
4227+
{
4228+
'prose-utility': (value) => ({ '--container-size': value }),
41984229
},
41994230
{
42004231
values: {
@@ -4210,18 +4241,40 @@ describe('matchComponents()', () => {
42104241
},
42114242
)
42124243

4213-
expect(optimizeCss(compiled.build(['prose', 'sm:prose-sm', 'hover:prose-lg'])).trim())
4214-
.toMatchInlineSnapshot(`
4215-
".prose {
4244+
expect(
4245+
optimizeCss(
4246+
compiled.build([
4247+
'prose-component',
4248+
'sm:prose-component',
4249+
'hover:prose-component',
4250+
'prose-utility',
4251+
'sm:prose-utility',
4252+
'hover:prose-utility',
4253+
]),
4254+
).trim(),
4255+
).toMatchInlineSnapshot(`
4256+
"@layer components {
4257+
.prose-component {
42164258
--container-size: normal;
42174259
}
4260+
}
42184261
4219-
@media (hover: hover) {
4220-
.hover\\:prose-lg:hover {
4221-
--container-size: lg;
4262+
.prose-utility {
4263+
--container-size: normal;
4264+
}
4265+
4266+
@media (hover: hover) {
4267+
@layer components {
4268+
.hover\\:prose-component:hover {
4269+
--container-size: normal;
42224270
}
4223-
}"
4224-
`)
4271+
}
4272+
4273+
.hover\\:prose-utility:hover {
4274+
--container-size: normal;
4275+
}
4276+
}"
4277+
`)
42254278
})
42264279
})
42274280

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

+26-2
Original file line numberDiff line numberDiff line change
@@ -448,11 +448,35 @@ export function buildPluginApi({
448448
},
449449

450450
addComponents(components, options) {
451-
this.addUtilities(components, options)
451+
function wrapRecord(record: Record<string, CssInJs>) {
452+
return Object.fromEntries(
453+
Object.entries(record).map(([key, value]) => [
454+
key,
455+
{
456+
'@layer components': value,
457+
},
458+
]),
459+
)
460+
}
461+
462+
this.addUtilities(
463+
Array.isArray(components) ? components.map(wrapRecord) : wrapRecord(components),
464+
options,
465+
)
452466
},
453467

454468
matchComponents(components, options) {
455-
this.matchUtilities(components, options)
469+
this.matchUtilities(
470+
Object.fromEntries(
471+
Object.entries(components).map(([key, fn]) => [
472+
key,
473+
(value, extra) => ({
474+
'@layer components': fn(value, extra),
475+
}),
476+
]),
477+
),
478+
options,
479+
)
456480
},
457481

458482
theme: createThemeFn(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { PluginAPI } from '../../src/compat/plugin-api'
2+
3+
export function plugin({ addComponents, addUtilities }: PluginAPI) {
4+
addUtilities({
5+
'.btn-utility': {
6+
padding: '1rem 2rem',
7+
borderRadius: '1rem',
8+
},
9+
})
10+
addComponents({
11+
'.btn': {
12+
padding: '.5rem 1rem',
13+
borderRadius: '.25rem',
14+
},
15+
'.btn-blue': {
16+
backgroundColor: '#3490dc',
17+
color: '#fff',
18+
'&:hover': {
19+
backgroundColor: '#2779bd',
20+
},
21+
},
22+
})
23+
}

packages/tailwindcss/tests/ui.spec.ts

+86-21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fs from 'node:fs'
44
import path from 'node:path'
55
import { optimize } from '../../@tailwindcss-node/src/optimize'
66
import { compile } from '../src'
7+
import type { PluginAPI } from '../src/plugin'
78
import { segment } from '../src/utils/segment'
89

910
const html = String.raw
@@ -2177,33 +2178,97 @@ test('shadow DOM has access to variables', async ({ page }) => {
21772178
expect(gap).toBe('8px')
21782179
})
21792180

2181+
test('legacy JS plugins will place components into a sub-layer', async ({ page }) => {
2182+
let { getPropertyValue } = await render(
2183+
page,
2184+
html`
2185+
<div id="a" class="btn btn-utility hover:btn-round"></div>
2186+
<div id="b" class="btn p-1 hover:btn-round"></div>
2187+
`,
2188+
css`
2189+
@plugin './fixtures/example-plugin.ts';
2190+
`,
2191+
{
2192+
async loadModule(id, base) {
2193+
return {
2194+
base,
2195+
module: ({ addComponents, addUtilities }: PluginAPI) => {
2196+
addUtilities({
2197+
'.btn-utility': {
2198+
padding: '1rem 2rem',
2199+
2200+
// Some additional properties so it would rank higher than the `.btn`
2201+
fontSize: '1.5rem',
2202+
fontWeight: 'bold',
2203+
lineHeight: '2',
2204+
},
2205+
})
2206+
addComponents({
2207+
'.btn': {
2208+
padding: '.5rem 1rem',
2209+
borderRadius: '.25rem',
2210+
},
2211+
'.btn-round': {
2212+
padding: '3rem',
2213+
borderRadius: '2rem',
2214+
},
2215+
})
2216+
},
2217+
}
2218+
},
2219+
},
2220+
)
2221+
2222+
expect(await getPropertyValue('#a', 'padding')).toEqual('16px 32px')
2223+
expect(await getPropertyValue('#a', 'border-radius')).toEqual('4px')
2224+
expect(await getPropertyValue('#a', 'line-height')).toEqual('48px')
2225+
await page.locator('#a').hover()
2226+
expect(await getPropertyValue('#a', 'padding')).toEqual('16px 32px')
2227+
expect(await getPropertyValue('#a', 'border-radius')).toEqual('32px')
2228+
expect(await getPropertyValue('#a', 'line-height')).toEqual('48px')
2229+
2230+
expect(await getPropertyValue('#b', 'padding')).toEqual('4px')
2231+
expect(await getPropertyValue('#b', 'border-radius')).toEqual('4px')
2232+
await page.locator('#b').hover()
2233+
expect(await getPropertyValue('#b', 'padding')).toEqual('4px')
2234+
expect(await getPropertyValue('#b', 'border-radius')).toEqual('32px')
2235+
})
2236+
21802237
// ---
21812238

21822239
const preflight = fs.readFileSync(path.resolve(__dirname, '..', 'preflight.css'), 'utf-8')
21832240
const defaultTheme = fs.readFileSync(path.resolve(__dirname, '..', 'theme.css'), 'utf-8')
21842241

2185-
async function render(page: Page, content: string, extraCss: string = '') {
2186-
let { build } = await compile(css`
2187-
@layer theme, base, components, utilities;
2188-
@layer theme {
2189-
${defaultTheme}
2190-
2191-
@theme {
2192-
--color-red: rgb(255, 0, 0);
2193-
--color-green: rgb(0, 255, 0);
2194-
--color-blue: rgb(0, 0, 255);
2195-
--color-black: black;
2196-
--color-transparent: transparent;
2242+
async function render(
2243+
page: Page,
2244+
content: string,
2245+
extraCss: string = '',
2246+
opts: Parameters<typeof compile>[1] = {},
2247+
) {
2248+
let { build } = await compile(
2249+
css`
2250+
@layer theme, base, components, utilities;
2251+
@layer theme {
2252+
${defaultTheme}
2253+
2254+
@theme {
2255+
--color-red: rgb(255, 0, 0);
2256+
--color-green: rgb(0, 255, 0);
2257+
--color-blue: rgb(0, 0, 255);
2258+
--color-black: black;
2259+
--color-transparent: transparent;
2260+
}
21972261
}
2198-
}
2199-
@layer base {
2200-
${preflight}
2201-
}
2202-
@layer utilities {
2203-
@tailwind utilities;
2204-
}
2205-
${extraCss}
2206-
`)
2262+
@layer base {
2263+
${preflight}
2264+
}
2265+
@layer utilities {
2266+
@tailwind utilities;
2267+
}
2268+
${extraCss}
2269+
`,
2270+
opts,
2271+
)
22072272

22082273
// We noticed that some of the tests depending on the `hover:` variant were
22092274
// flaky. After some investigation, we found that injected elements had the

0 commit comments

Comments
 (0)