Skip to content

Put components into an extra layer when using the legacy JS API #17918

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix incorrectly replacing `_` with ` ` in arbitrary modifier shorthand `bg-red-500/(--my_opacity)` ([#17889](https://github.com/tailwindlabs/tailwindcss/pull/17889))
- Upgrade: Bump dependencies in parallel and make the upgrade faster ([#17898](https://github.com/tailwindlabs/tailwindcss/pull/17898))
- Don't scan `.log` files for classes by default ([#17906](https://github.com/tailwindlabs/tailwindcss/pull/17906))
- 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))

## [4.1.5] - 2025-04-30

Expand Down
119 changes: 86 additions & 33 deletions packages/tailwindcss/src/compat/plugin-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4113,17 +4113,19 @@ describe('matchUtilities()', () => {
})

describe('addComponents()', () => {
test('is an alias for addUtilities', async () => {
test('is an alias for addUtilities that wraps the code in \`@layer components\`', async () => {
let compiled = await compile(
css`
@plugin "my-plugin";
@tailwind utilities;
@layer utilities {
@tailwind utilities;
}
`,
{
async loadModule(id, base) {
return {
base,
module: ({ addComponents }: PluginAPI) => {
module: ({ addComponents, addUtilities }: PluginAPI) => {
addComponents({
'.btn': {
padding: '.5rem 1rem',
Expand All @@ -4145,43 +4147,60 @@ describe('addComponents()', () => {
},
},
})
addUtilities({
'.btn-utility': {
padding: '.5rem 1rem',
borderRadius: '.25rem',
fontWeight: '600',
},
})
},
}
},
},
)

expect(optimizeCss(compiled.build(['btn', 'btn-blue', 'btn-red'])).trim())
expect(optimizeCss(compiled.build(['btn', 'btn-blue', 'btn-red', 'btn-utility'])).trim())
.toMatchInlineSnapshot(`
".btn {
border-radius: .25rem;
padding: .5rem 1rem;
font-weight: 600;
}
"@layer utilities {
@layer components {
.btn {
border-radius: .25rem;
padding: .5rem 1rem;
font-weight: 600;
}

.btn-blue {
color: #fff;
background-color: #3490dc;
}
.btn-blue {
color: #fff;
background-color: #3490dc;
}

.btn-blue:hover {
background-color: #2779bd;
}
.btn-blue:hover {
background-color: #2779bd;
}

.btn-red {
color: #fff;
background-color: #e3342f;
}
.btn-red {
color: #fff;
background-color: #e3342f;
}

.btn-red:hover {
background-color: #cc1f1a;
.btn-red:hover {
background-color: #cc1f1a;
}
}

.btn-utility {
border-radius: .25rem;
padding: .5rem 1rem;
font-weight: 600;
}
}"
`)
})
})

describe('matchComponents()', () => {
test('is an alias for matchUtilities', async () => {
test('is an alias for matchUtilities that wraps the code in \`@layer components\`', async () => {
let compiled = await compile(
css`
@plugin "my-plugin";
Expand All @@ -4191,10 +4210,22 @@ describe('matchComponents()', () => {
async loadModule(id, base) {
return {
base,
module: ({ matchComponents }: PluginAPI) => {
module: ({ matchComponents, matchUtilities }: PluginAPI) => {
matchComponents(
{
prose: (value) => ({ '--container-size': value }),
'prose-component': (value) => ({ '--container-size': value }),
},
{
values: {
DEFAULT: 'normal',
sm: 'sm',
lg: 'lg',
},
},
)
matchUtilities(
{
'prose-utility': (value) => ({ '--container-size': value }),
},
{
values: {
Expand All @@ -4210,18 +4241,40 @@ describe('matchComponents()', () => {
},
)

expect(optimizeCss(compiled.build(['prose', 'sm:prose-sm', 'hover:prose-lg'])).trim())
.toMatchInlineSnapshot(`
".prose {
expect(
optimizeCss(
compiled.build([
'prose-component',
'sm:prose-component',
'hover:prose-component',
'prose-utility',
'sm:prose-utility',
'hover:prose-utility',
]),
).trim(),
).toMatchInlineSnapshot(`
"@layer components {
.prose-component {
--container-size: normal;
}
}

@media (hover: hover) {
.hover\\:prose-lg:hover {
--container-size: lg;
.prose-utility {
--container-size: normal;
}

@media (hover: hover) {
@layer components {
.hover\\:prose-component:hover {
--container-size: normal;
}
}"
`)
}

.hover\\:prose-utility:hover {
--container-size: normal;
}
}"
`)
})
})

Expand Down
28 changes: 26 additions & 2 deletions packages/tailwindcss/src/compat/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,11 +448,35 @@ export function buildPluginApi({
},

addComponents(components, options) {
this.addUtilities(components, options)
function wrapRecord(record: Record<string, CssInJs>) {
return Object.fromEntries(
Object.entries(record).map(([key, value]) => [
key,
{
'@layer components': value,
},
]),
)
}

this.addUtilities(
Array.isArray(components) ? components.map(wrapRecord) : wrapRecord(components),
options,
)
},

matchComponents(components, options) {
this.matchUtilities(components, options)
this.matchUtilities(
Object.fromEntries(
Object.entries(components).map(([key, fn]) => [
key,
(value, extra) => ({
'@layer components': fn(value, extra),
}),
]),
),
options,
)
},

theme: createThemeFn(
Expand Down
23 changes: 23 additions & 0 deletions packages/tailwindcss/tests/fixtures/example-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { PluginAPI } from '../../src/compat/plugin-api'

export function plugin({ addComponents, addUtilities }: PluginAPI) {
addUtilities({
'.btn-utility': {
padding: '1rem 2rem',
borderRadius: '1rem',
},
})
addComponents({
'.btn': {
padding: '.5rem 1rem',
borderRadius: '.25rem',
},
'.btn-blue': {
backgroundColor: '#3490dc',
color: '#fff',
'&:hover': {
backgroundColor: '#2779bd',
},
},
})
}
107 changes: 86 additions & 21 deletions packages/tailwindcss/tests/ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { optimize } from '../../@tailwindcss-node/src/optimize'
import { compile } from '../src'
import type { PluginAPI } from '../src/plugin'
import { segment } from '../src/utils/segment'

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

test('legacy JS plugins will place components into a sub-layer', async ({ page }) => {
let { getPropertyValue } = await render(
page,
html`
<div id="a" class="btn btn-utility hover:btn-round"></div>
<div id="b" class="btn p-1 hover:btn-round"></div>
`,
css`
@plugin './fixtures/example-plugin.ts';
`,
{
async loadModule(id, base) {
return {
base,
module: ({ addComponents, addUtilities }: PluginAPI) => {
addUtilities({
'.btn-utility': {
padding: '1rem 2rem',

// Some additional properties so it would rank higher than the `.btn`
fontSize: '1.5rem',
fontWeight: 'bold',
lineHeight: '2',
},
})
addComponents({
'.btn': {
padding: '.5rem 1rem',
borderRadius: '.25rem',
},
'.btn-round': {
padding: '3rem',
borderRadius: '2rem',
},
})
},
}
},
},
)

expect(await getPropertyValue('#a', 'padding')).toEqual('16px 32px')
expect(await getPropertyValue('#a', 'border-radius')).toEqual('4px')
expect(await getPropertyValue('#a', 'line-height')).toEqual('48px')
await page.locator('#a').hover()
expect(await getPropertyValue('#a', 'padding')).toEqual('16px 32px')
expect(await getPropertyValue('#a', 'border-radius')).toEqual('32px')
expect(await getPropertyValue('#a', 'line-height')).toEqual('48px')

expect(await getPropertyValue('#b', 'padding')).toEqual('4px')
expect(await getPropertyValue('#b', 'border-radius')).toEqual('4px')
await page.locator('#b').hover()
expect(await getPropertyValue('#b', 'padding')).toEqual('4px')
expect(await getPropertyValue('#b', 'border-radius')).toEqual('32px')
})

// ---

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

async function render(page: Page, content: string, extraCss: string = '') {
let { build } = await compile(css`
@layer theme, base, components, utilities;
@layer theme {
${defaultTheme}

@theme {
--color-red: rgb(255, 0, 0);
--color-green: rgb(0, 255, 0);
--color-blue: rgb(0, 0, 255);
--color-black: black;
--color-transparent: transparent;
async function render(
page: Page,
content: string,
extraCss: string = '',
opts: Parameters<typeof compile>[1] = {},
) {
let { build } = await compile(
css`
@layer theme, base, components, utilities;
@layer theme {
${defaultTheme}

@theme {
--color-red: rgb(255, 0, 0);
--color-green: rgb(0, 255, 0);
--color-blue: rgb(0, 0, 255);
--color-black: black;
--color-transparent: transparent;
}
}
}
@layer base {
${preflight}
}
@layer utilities {
@tailwind utilities;
}
${extraCss}
`)
@layer base {
${preflight}
}
@layer utilities {
@tailwind utilities;
}
${extraCss}
`,
opts,
)

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