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:
Philipp Spiess 2024-11-13 19:39:20 +01:00 committed by GitHub
parent dda181b833
commit dd85aadc2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 299 additions and 54 deletions

View File

@ -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

View File

@ -3375,6 +3375,7 @@ exports[`getClassList 1`] = `
"contain-size",
"contain-strict",
"contain-style",
"container",
"content-around",
"content-baseline",
"content-between",

View File

@ -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',

View File

@ -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;

View File

@ -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`
*/

View 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
}

View File

@ -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(