mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Reintroduce container component as a utility (#14993)
Closes #13129 We're adding back the v3 `container` component, this time as a utility. The idea is that we support the default `container` behavior but we will not have an API to configure this similar to what v3 offered. Instead, the recommended approach is to configure it by creating a custom utility like so: ```css @import "tailwindcss"; @utility container { margin-left: auto; margin-right: auto; padding-left: 2rem; padding-right: 2rem; } ``` We do have an idea of how to migrate existing JS configuration files to the new `@utility` as part of the interop layer and the codemod. This is going to be a follow-up PR though. ## Test Plan We added a unit test but we've also played around with it in the Vite playground. Yep, looks like a `container`: https://github.com/user-attachments/assets/ea7a5a4c-4cde-4ef5-9062-03e16239eb85 --------- Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This commit is contained in:
parent
dda181b833
commit
dd85aadc2c
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Support opacity values in increments of `0.25` by default ([#14980](https://github.com/tailwindlabs/tailwindcss/pull/14980))
|
||||
- Support specifying the color interpolation method for gradients via modifier ([#14984](https://github.com/tailwindlabs/tailwindcss/pull/14984))
|
||||
- Reintroduce `container` component as a utility ([#14993](https://github.com/tailwindlabs/tailwindcss/pull/14993))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@ -3375,6 +3375,7 @@ exports[`getClassList 1`] = `
|
||||
"contain-size",
|
||||
"contain-strict",
|
||||
"contain-style",
|
||||
"container",
|
||||
"content-around",
|
||||
"content-baseline",
|
||||
"content-between",
|
||||
|
||||
@ -28,6 +28,10 @@ export default [
|
||||
'float',
|
||||
'clear',
|
||||
|
||||
// Ensure that the included `container` class is always sorted before any
|
||||
// custom container extensions
|
||||
'--tw-container-component',
|
||||
|
||||
// How do we make `mx-0` come before `mt-0`?
|
||||
// Idea: `margin-x` property that we compile away with a Visitor plugin?
|
||||
'margin',
|
||||
|
||||
@ -3148,6 +3148,230 @@ test('max-height', async () => {
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
describe('container', () => {
|
||||
test('creates the right media queries and sorts it before width', async () => {
|
||||
expect(
|
||||
await compileCss(
|
||||
css`
|
||||
@theme {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['w-1/2', 'container', 'max-w-[var(--breakpoint-sm)]'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (width >= 40rem) {
|
||||
.container {
|
||||
max-width: 40rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 48rem) {
|
||||
.container {
|
||||
max-width: 48rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 64rem) {
|
||||
.container {
|
||||
max-width: 64rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 80rem) {
|
||||
.container {
|
||||
max-width: 80rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 96rem) {
|
||||
.container {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
|
||||
.w-1\\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.max-w-\\[var\\(--breakpoint-sm\\)\\] {
|
||||
max-width: var(--breakpoint-sm);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('sorts breakpoints based on unit and then in ascending aOrder', async () => {
|
||||
expect(
|
||||
await compileCss(
|
||||
css`
|
||||
@theme reference {
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-3xl: 1600px;
|
||||
--breakpoint-sm: 40em;
|
||||
--breakpoint-2xl: 96rem;
|
||||
--breakpoint-xs: 30px;
|
||||
--breakpoint-md: 48em;
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['container'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
".container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (width >= 40em) {
|
||||
.container {
|
||||
max-width: 40em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 48em) {
|
||||
.container {
|
||||
max-width: 48em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 30px) {
|
||||
.container {
|
||||
max-width: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1600px) {
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 64rem) {
|
||||
.container {
|
||||
max-width: 64rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 80rem) {
|
||||
.container {
|
||||
max-width: 80rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 96rem) {
|
||||
.container {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('custom `@utility container` always follow the core utility ', async () => {
|
||||
expect(
|
||||
await compileCss(
|
||||
css`
|
||||
@theme {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
@tailwind utilities;
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 1rem;
|
||||
|
||||
@media (width >= theme(--breakpoint-sm)) {
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
['w-1/2', 'container', 'max-w-[var(--breakpoint-sm)]'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
":root {
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (width >= 40rem) {
|
||||
.container {
|
||||
max-width: 40rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 48rem) {
|
||||
.container {
|
||||
max-width: 48rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 64rem) {
|
||||
.container {
|
||||
max-width: 64rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 80rem) {
|
||||
.container {
|
||||
max-width: 80rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 96rem) {
|
||||
.container {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
@media (width >= 40rem) {
|
||||
.container {
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.w-1\\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.max-w-\\[var\\(--breakpoint-sm\\)\\] {
|
||||
max-width: var(--breakpoint-sm);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
test('flex', async () => {
|
||||
expect(
|
||||
await run([
|
||||
@ -16680,7 +16904,7 @@ describe('spacing utilities', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
test('only multiples of 0.25 with no trailing zeroes are supported with the spacing multipler', async () => {
|
||||
test('only multiples of 0.25 with no trailing zeroes are supported with the spacing multiplier', async () => {
|
||||
let { build } = await compile(css`
|
||||
@theme {
|
||||
--spacing: 4px;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { atRoot, atRule, decl, styleRule, type AstNode } from './ast'
|
||||
import type { Candidate, CandidateModifier, NamedUtilityValue } from './candidate'
|
||||
import type { Theme, ThemeKey } from './theme'
|
||||
import { compareBreakpoints } from './utils/compare-breakpoints'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
import {
|
||||
inferDataType,
|
||||
@ -897,6 +898,18 @@ export function createUtilities(theme: Theme) {
|
||||
})
|
||||
}
|
||||
|
||||
utilities.static('container', () => {
|
||||
let breakpoints = [...theme.namespace('--breakpoint').values()]
|
||||
breakpoints.sort((a, z) => compareBreakpoints(a, z, 'asc'))
|
||||
|
||||
let decls: AstNode[] = [decl('--tw-sort', '--tw-container-component'), decl('width', '100%')]
|
||||
for (let breakpoint of breakpoints) {
|
||||
decls.push(atRule('@media', `(min-width: ${breakpoint})`, [decl('max-width', breakpoint)]))
|
||||
}
|
||||
|
||||
return decls
|
||||
})
|
||||
|
||||
/**
|
||||
* @css `flex`
|
||||
*/
|
||||
|
||||
48
packages/tailwindcss/src/utils/compare-breakpoints.ts
Normal file
48
packages/tailwindcss/src/utils/compare-breakpoints.ts
Normal file
@ -0,0 +1,48 @@
|
||||
export function compareBreakpoints(a: string, z: string, direction: 'asc' | 'desc') {
|
||||
if (a === z) return 0
|
||||
|
||||
// Assumption: when a `(` exists, we are dealing with a CSS function.
|
||||
//
|
||||
// E.g.: `calc(100% - 1rem)`
|
||||
let aIsCssFunction = a.indexOf('(')
|
||||
let zIsCssFunction = z.indexOf('(')
|
||||
|
||||
let aBucket =
|
||||
aIsCssFunction === -1
|
||||
? // No CSS function found, bucket by unit instead
|
||||
a.replace(/[\d.]+/g, '')
|
||||
: // CSS function found, bucket by function name
|
||||
a.slice(0, aIsCssFunction)
|
||||
|
||||
let zBucket =
|
||||
zIsCssFunction === -1
|
||||
? // No CSS function found, bucket by unit
|
||||
z.replace(/[\d.]+/g, '')
|
||||
: // CSS function found, bucket by function name
|
||||
z.slice(0, zIsCssFunction)
|
||||
|
||||
let order =
|
||||
// Compare by bucket name
|
||||
(aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) ||
|
||||
// If bucket names are the same, compare by value
|
||||
(direction === 'asc' ? parseInt(a) - parseInt(z) : parseInt(z) - parseInt(a))
|
||||
|
||||
// If the groups are the same, and the contents are not numbers, the
|
||||
// `order` will result in `NaN`. In this case, we want to make sorting
|
||||
// stable by falling back to a string comparison.
|
||||
//
|
||||
// This can happen when using CSS functions such as `calc`.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// - `min-[calc(100%-1rem)]` and `min-[calc(100%-2rem)]`
|
||||
// - `@[calc(100%-1rem)]` and `@[calc(100%-2rem)]`
|
||||
//
|
||||
// In this scenario, we want to alphabetically sort `calc(100%-1rem)` and
|
||||
// `calc(100%-2rem)` to make it deterministic.
|
||||
if (Number.isNaN(order)) {
|
||||
return a < z ? -1 : 1
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
@ -13,6 +13,7 @@ import {
|
||||
} from './ast'
|
||||
import { type Variant } from './candidate'
|
||||
import type { Theme } from './theme'
|
||||
import { compareBreakpoints } from './utils/compare-breakpoints'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
import { isPositiveInteger } from './utils/infer-data-type'
|
||||
import { segment } from './utils/segment'
|
||||
@ -869,7 +870,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
// Helper to compare variants by their resolved values, this is used by the
|
||||
// responsive variants (`sm`, `md`, ...), `min-*`, `max-*` and container
|
||||
// queries (`@`).
|
||||
function compareBreakpoints(
|
||||
function compareBreakpointVariants(
|
||||
a: Variant,
|
||||
z: Variant,
|
||||
direction: 'asc' | 'desc',
|
||||
@ -882,54 +883,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
let zValue = lookup.get(z)
|
||||
if (zValue === null) return direction === 'asc' ? 1 : -1
|
||||
|
||||
if (aValue === zValue) return 0
|
||||
|
||||
// Assumption: when a `(` exists, we are dealing with a CSS function.
|
||||
//
|
||||
// E.g.: `calc(100% - 1rem)`
|
||||
let aIsCssFunction = aValue.indexOf('(')
|
||||
let zIsCssFunction = zValue.indexOf('(')
|
||||
|
||||
let aBucket =
|
||||
aIsCssFunction === -1
|
||||
? // No CSS function found, bucket by unit instead
|
||||
aValue.replace(/[\d.]+/g, '')
|
||||
: // CSS function found, bucket by function name
|
||||
aValue.slice(0, aIsCssFunction)
|
||||
|
||||
let zBucket =
|
||||
zIsCssFunction === -1
|
||||
? // No CSS function found, bucket by unit
|
||||
zValue.replace(/[\d.]+/g, '')
|
||||
: // CSS function found, bucket by function name
|
||||
zValue.slice(0, zIsCssFunction)
|
||||
|
||||
let order =
|
||||
// Compare by bucket name
|
||||
(aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) ||
|
||||
// If bucket names are the same, compare by value
|
||||
(direction === 'asc'
|
||||
? parseInt(aValue) - parseInt(zValue)
|
||||
: parseInt(zValue) - parseInt(aValue))
|
||||
|
||||
// If the groups are the same, and the contents are not numbers, the
|
||||
// `order` will result in `NaN`. In this case, we want to make sorting
|
||||
// stable by falling back to a string comparison.
|
||||
//
|
||||
// This can happen when using CSS functions such as `calc`.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// - `min-[calc(100%-1rem)]` and `min-[calc(100%-2rem)]`
|
||||
// - `@[calc(100%-1rem)]` and `@[calc(100%-2rem)]`
|
||||
//
|
||||
// In this scenario, we want to alphabetically sort `calc(100%-1rem)` and
|
||||
// `calc(100%-2rem)` to make it deterministic.
|
||||
if (Number.isNaN(order)) {
|
||||
return aValue < zValue ? -1 : 1
|
||||
}
|
||||
|
||||
return order
|
||||
return compareBreakpoints(aValue, zValue, direction)
|
||||
}
|
||||
|
||||
// Breakpoints
|
||||
@ -978,7 +932,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
},
|
||||
(a, z) => compareBreakpoints(a, z, 'desc', resolvedBreakpoints),
|
||||
(a, z) => compareBreakpointVariants(a, z, 'desc', resolvedBreakpoints),
|
||||
)
|
||||
|
||||
variants.suggest(
|
||||
@ -1013,7 +967,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
},
|
||||
(a, z) => compareBreakpoints(a, z, 'asc', resolvedBreakpoints),
|
||||
(a, z) => compareBreakpointVariants(a, z, 'asc', resolvedBreakpoints),
|
||||
)
|
||||
|
||||
variants.suggest(
|
||||
@ -1072,7 +1026,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
},
|
||||
(a, z) => compareBreakpoints(a, z, 'desc', resolvedWidths),
|
||||
(a, z) => compareBreakpointVariants(a, z, 'desc', resolvedWidths),
|
||||
)
|
||||
|
||||
variants.suggest(
|
||||
@ -1119,7 +1073,7 @@ export function createVariants(theme: Theme): Variants {
|
||||
{ compounds: Compounds.AtRules },
|
||||
)
|
||||
},
|
||||
(a, z) => compareBreakpoints(a, z, 'asc', resolvedWidths),
|
||||
(a, z) => compareBreakpointVariants(a, z, 'asc', resolvedWidths),
|
||||
)
|
||||
|
||||
variants.suggest(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user