Skip to content

Commit 890f18d

Browse files
Add codemod and interop for legacy container component configu (#14999)
This PR adds support for handling v3 [`container` customizations ](https://tailwindcss.com/docs/container#customizing). This is done by adding a custom utility to extend the core `container` utility. A concrete example can be taken from the added integration test. ### Input ```ts /** @type {import('tailwindcss').Config} */ export default { content: ['./src/**/*.html'], theme: { container: { center: true, padding: { DEFAULT: '2rem', '2xl': '4rem', }, screens: { md: '48rem', // Matches a default --breakpoint xl: '1280px', '2xl': '1536px', }, }, }, } ``` ### Output ```css @import "tailwindcss"; @Utility container { margin-inline: auto; padding-inline: 2rem; @media (width >= theme(--breakpoint-sm)) { max-width: none; } @media (width >= 48rem) { max-width: 48rem; } @media (width >= 1280px) { max-width: 1280px; } @media (width >= 1536px) { max-width: 1536px; padding-inline: 4rem; } } ```` ## Test Plan This PR adds extensive tests to the compat layer as part of unit tests. Additionally it does at a test to the codemod setup that shows that the right `@utility` code is generated. Furthermore I compared the implementation against v3 on both the compat layer and the custom `@utility`: https://github.com/user-attachments/assets/44d6cbfb-4861-4225-9593-602b719f628f
1 parent 4079059 commit 890f18d

File tree

8 files changed

+834
-3
lines changed

8 files changed

+834
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Support opacity values in increments of `0.25` by default ([#14980](https://github.com/tailwindlabs/tailwindcss/pull/14980))
1313
- Support specifying the color interpolation method for gradients via modifier ([#14984](https://github.com/tailwindlabs/tailwindcss/pull/14984))
14-
- Reintroduce `container` component as a utility ([#14993](https://github.com/tailwindlabs/tailwindcss/pull/14993))
14+
- Reintroduce `container` component as a utility ([#14993](https://github.com/tailwindlabs/tailwindcss/pull/14993), [#14999](https://github.com/tailwindlabs/tailwindcss/pull/14999))
15+
- _Upgrade (experimental)_: Migrate `container` component configuration to CSS ([#14999](https://github.com/tailwindlabs/tailwindcss/pull/14999))
1516

1617
### Fixed
1718

integrations/upgrade/js-config.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,4 +1435,97 @@ describe('border compatibility', () => {
14351435
`)
14361436
},
14371437
)
1438+
1439+
test(
1440+
'migrates `container` component configurations',
1441+
{
1442+
fs: {
1443+
'package.json': json`
1444+
{
1445+
"dependencies": {
1446+
"@tailwindcss/upgrade": "workspace:^"
1447+
}
1448+
}
1449+
`,
1450+
'tailwind.config.ts': ts`
1451+
import { type Config } from 'tailwindcss'
1452+
1453+
export default {
1454+
content: ['./src/**/*.html'],
1455+
theme: {
1456+
container: {
1457+
center: true,
1458+
padding: {
1459+
DEFAULT: '2rem',
1460+
'2xl': '4rem',
1461+
},
1462+
screens: {
1463+
md: '48rem', // Matches a default --breakpoint
1464+
xl: '1280px',
1465+
'2xl': '1536px',
1466+
},
1467+
},
1468+
},
1469+
} satisfies Config
1470+
`,
1471+
'src/input.css': css`
1472+
@tailwind base;
1473+
@tailwind components;
1474+
@tailwind utilities;
1475+
`,
1476+
'src/index.html': html`
1477+
<div class="container"></div>
1478+
`,
1479+
},
1480+
},
1481+
async ({ exec, fs }) => {
1482+
await exec('npx @tailwindcss/upgrade')
1483+
1484+
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
1485+
"
1486+
--- src/index.html ---
1487+
<div class="container"></div>
1488+
1489+
--- src/input.css ---
1490+
@import 'tailwindcss';
1491+
1492+
@utility container {
1493+
margin-inline: auto;
1494+
padding-inline: 2rem;
1495+
@media (width >= theme(--breakpoint-sm)) {
1496+
max-width: none;
1497+
}
1498+
@media (width >= 48rem) {
1499+
max-width: 48rem;
1500+
}
1501+
@media (width >= 1280px) {
1502+
max-width: 1280px;
1503+
}
1504+
@media (width >= 1536px) {
1505+
max-width: 1536px;
1506+
padding-inline: 4rem;
1507+
}
1508+
}
1509+
1510+
/*
1511+
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
1512+
so we've added these compatibility styles to make sure everything still
1513+
looks the same as it did with Tailwind CSS v3.
1514+
1515+
If we ever want to remove these styles, we need to add an explicit border
1516+
color utility to any element that depends on these defaults.
1517+
*/
1518+
@layer base {
1519+
*,
1520+
::after,
1521+
::before,
1522+
::backdrop,
1523+
::file-selector-button {
1524+
border-color: var(--color-gray-200, currentColor);
1525+
}
1526+
}
1527+
"
1528+
`)
1529+
},
1530+
)
14381531
})

packages/@tailwindcss-upgrade/src/migrate-js-config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import { fileURLToPath } from 'node:url'
55
import { type Config } from 'tailwindcss'
66
import defaultTheme from 'tailwindcss/defaultTheme'
77
import { loadModule } from '../../@tailwindcss-node/src/compile'
8-
import { toCss, type AstNode } from '../../tailwindcss/src/ast'
8+
import { atRule, toCss, type AstNode } from '../../tailwindcss/src/ast'
99
import {
1010
keyPathToCssProperty,
1111
themeableValues,
1212
} from '../../tailwindcss/src/compat/apply-config-to-theme'
1313
import { keyframesToRules } from '../../tailwindcss/src/compat/apply-keyframes-to-theme'
1414
import { resolveConfig, type ConfigFile } from '../../tailwindcss/src/compat/config/resolve-config'
1515
import type { ResolvedConfig, ThemeConfig } from '../../tailwindcss/src/compat/config/types'
16+
import { buildCustomContainerUtilityRules } from '../../tailwindcss/src/compat/container'
1617
import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
1718
import type { DesignSystem } from '../../tailwindcss/src/design-system'
1819
import { escape } from '../../tailwindcss/src/utils/escape'
@@ -148,6 +149,14 @@ async function migrateTheme(
148149
}
149150

150151
css += '}\n' // @theme
152+
153+
if ('container' in resolvedConfig.theme) {
154+
let rules = buildCustomContainerUtilityRules(resolvedConfig.theme.container, designSystem)
155+
if (rules.length > 0) {
156+
css += '\n' + toCss([atRule('@utility', 'container', rules)])
157+
}
158+
}
159+
151160
css += '}\n' // @tw-bucket
152161

153162
return css

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { applyKeyframesToTheme } from './apply-keyframes-to-theme'
66
import { createCompatConfig } from './config/create-compat-config'
77
import { resolveConfig } from './config/resolve-config'
88
import type { UserConfig } from './config/types'
9+
import { registerContainerCompat } from './container'
910
import { darkModePlugin } from './dark-mode'
1011
import { buildPluginApi, type CssPluginOptions, type Plugin } from './plugin-api'
1112
import { registerScreensConfig } from './screens-config'
@@ -239,6 +240,7 @@ function upgradeToFullPluginSupport({
239240

240241
registerThemeVariantOverrides(resolvedUserConfig, designSystem)
241242
registerScreensConfig(resolvedUserConfig, designSystem)
243+
registerContainerCompat(resolvedUserConfig, designSystem)
242244

243245
// If a prefix has already been set in CSS don't override it
244246
if (!designSystem.theme.prefix && resolvedConfig.prefix) {

packages/tailwindcss/src/compat/apply-config-to-theme.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ export function themeableValues(config: ResolvedConfig['theme']): [string[], unk
128128
const IS_VALID_KEY = /^[a-zA-Z0-9-_%/\.]+$/
129129

130130
export function keyPathToCssProperty(path: string[]) {
131+
// The legacy container component config should not be included in the Theme
132+
if (path[0] === 'container') return null
133+
131134
path = structuredClone(path)
132135

133136
if (path[0] === 'animation') path[0] = 'animate'

0 commit comments

Comments
 (0)