Tailwind Container in v4: What It Generates, and When to Override
The utility is width:100% plus a max-width per breakpoint — no centring, no padding. Read from v4.3.1's source, next to the 43 lines of CSS that replaced it.
Tailwind's container utility sets width: 100% plus one max-width media query per breakpoint in your theme — and nothing else. It does not centre itself and adds no horizontal padding. In Tailwind v4 it is also no longer configurable from JavaScript, which is why this codebase replaced it with 43 lines of plain CSS.
The short answer
container in Tailwind CSS v4 emits width: 100% followed by a @media (width >= …) { max-width: … } rule for every --breakpoint-* variable in your theme. Centring needs mx-auto, padding needs px-*. The v3 theme.container options (center, padding, screens) only work through a JavaScript config file, which a CSS-first v4 project does not have.
What the utility actually generates
This is not a paraphrase of the docs — it is the utility's definition, lifted from node_modules/tailwindcss/dist/lib.mjs in the pinned v4.3.1 this site builds against:
i.static("container", () => {
let a = [...e.namespace("--breakpoint").values()];
a.sort((w, C) => Te(w, C, "asc"));
let g = [o("--tw-sort", "--tw-container-component"), o("width", "100%")];
for (let w of a) g.push(B("@media", `(width >= ${w})`, [o("max-width", w)]));
return g;
});
Three things fall out of those six lines, and all three surprise people:
- No
margin-inline: auto. The utility never centres. Adiv.containersits flush left until you addmx-auto. - No padding. Content touches the viewport edge below the first breakpoint unless you add
px-4or similar. - It iterates the whole
--breakpointnamespace. Not a fixed list of five — every--breakpoint-*variable in your@theme. Add a custom one and it silently becomes a container tier.
That third point is the one that bites a project with custom breakpoints. src/app/globals.css here declares two extras:
@theme {
--breakpoint-2xsm: 375px;
--breakpoint-xsm: 425px;
}
If this site used Tailwind's container, those two would each add a max-width step at 375px and 425px — clamping a phone layout that was meant to be fluid. Nothing warns you; the utility just grew two tiers because the theme did.
Container, max-w-*, and container queries are three different things
Searching "tailwind container" returns all three, and they share almost nothing but the word.
| Thing | Syntax | What it keys off | What it does |
|---|---|---|---|
The container utility | class="container" | --breakpoint-* | width: 100% + a max-width step per breakpoint |
The --container-* scale | class="max-w-md" | --container-* | A fixed max-width (--container-md is 28rem) |
| Container queries | class="@container" + @md:flex | the element's own width | Styles children by the parent's width, not the viewport's |
The --container-* namespace is the confusing one: it is named "container" but powers max-w-*, and v4's own theme ships thirteen steps of it (--container-3xs: 16rem through --container-7xl: 80rem). max-w-md has nothing to do with class="container". If what you want is "this article column stops at 28rem," reach for max-w-md and skip the container utility entirely. Container queries are a separate feature again — see Tailwind container queries for where those earn their keep.
Why this codebase overrides it instead of configuring it
In Tailwind v3 you tuned the container in tailwind.config.js:
// v3 only — this file does not exist in a CSS-first v4 project
theme: {
container: { center: true, padding: "1rem", screens: { "2xl": "1536px" } },
}
v4 still reads that shape, but only from a JavaScript config loaded with @config. A CSS-first project has no such file — this repo has postcss.config.mjs and a @theme block in globals.css, and no tailwind.config.js at all. So center and padding are simply unavailable, and the storefront's .container is 43 lines of ordinary CSS in src/app/globals.css:
/* Responsive page container (matches source markup's .container) */
.container {
width: 100%;
margin-inline: auto;
padding-inline: 1rem;
}
@media (min-width: 425px) {
.container { max-width: 425px; }
}
@media (min-width: 640px) {
.container { max-width: 640px; }
}
@media (min-width: 768px) {
.container { max-width: 768px; }
}
@media (min-width: 1024px) {
.container { max-width: 1024px; }
}
@media (min-width: 1280px) {
.container { max-width: 1280px; }
}
@media (min-width: 1440px) {
.container { padding-inline: 4rem; }
}
@media (min-width: 1536px) {
.container { max-width: 1536px; padding-inline: 3rem; }
}
Two details in there are only expressible as hand-written CSS. The 1440px rule changes padding without changing max-width — between 1440px and 1535px the column stays at 1280px wide but the gutter grows to 4rem, which is what keeps a wide desktop from reading as a narrow strip floating in white space. And the gutter then shrinks to 3rem at 1536px, because the column itself jumps to 1536px and a 4rem gutter would overshoot. A padding value in the old v3 config is a single number per breakpoint; it cannot express "wider gutter here, narrower gutter one step later."
The override always wins — and ships twice
Tailwind v4's entry point opens with:
@layer theme, base, components, utilities;
Every generated utility lands inside @layer utilities. The .container rule above is declared outside any layer, and unlayered CSS beats layered CSS in the cascade regardless of source order or specificity. So even though both .container definitions exist in the built stylesheet, the hand-written one always applies. That is worth knowing before you debug a container override with !important — it was never a specificity problem.
It is also a cost, and this site has measured it: a stylesheet audit found 393 bytes of Tailwind's generated container shipping unreachable beside the 463-byte override, out of a 73,464-byte stylesheet. Small, but downloaded on every page, and it leaves eight media-query blocks a maintainer has to disambiguate from the eight that matter. The max-width census walks that build output rule by rule.
The way to get the custom rule without the duplicate is @utility, which defines the class inside Tailwind's own layer instead of shadowing it:
@utility container {
width: 100%;
margin-inline: auto;
padding-inline: 1rem;
/* …then the project's own tiers, with no second copy */
}
That is the clean version of everything above. The plain .container block this repo still ships predates the audit; it behaves identically in the browser and is simply 393 bytes less tidy.
The atom, not the class name
Only one file in src/ writes className="container" directly, and it is src/components/atoms/Container.tsx:
export default function Container({
className = "",
children,
}: {
className?: string;
children: ReactNode;
}) {
return <div className={`container ${className}`.trim()}>{children}</div>;
}
Thirty-one files import that atom. Every page section, the dashboard shell, the blog article column — all of them go through it rather than typing the class. That is the payoff of wrapping a single utility in a component: the page-width rule has exactly one call site to change, and a section that needs to opt out of it is visible in a diff as a missing <Container>, not as an absent string in a class list.
It also means the atom is where per-section overrides land, via the className passthrough, instead of a second bespoke container class accumulating somewhere. The Atomic Design layering this site follows — described in Atomic design for Next.js components — puts Container at the bottom as a pure, prop-driven primitive with no data imports, which is precisely why it can be the one thing every layout depends on.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
container isn't centred | v4's utility has no margin-inline: auto | Add mx-auto, or define your own .container with it baked in |
| Content touches the screen edge on mobile | The utility adds no padding | Add px-4 alongside container, or bake padding-inline into a custom rule |
| A custom breakpoint made the container narrower | The utility iterates every --breakpoint-* in the theme | Remove the breakpoint from @theme, or stop using the utility and hand-write the tiers |
theme.container.padding does nothing | v4 reads it only from a JS config loaded with @config; a CSS-first project has none | Write the rule in plain CSS, or add a JS config back |
An override in globals.css is ignored | Yours is also in @layer utilities, so order decides | Define it with @utility container so it replaces rather than competes |
| Both container rules appear in the built CSS | An unlayered .container shadows the generated one instead of replacing it | Switch the override to @utility container |
New @theme values don't generate utilities in dev | Tailwind v4 + Turbopack does not hot-reload @theme | Restart npm run dev after editing theme variables |
max-w-md changed when you edited breakpoints | It doesn't — max-w-* reads --container-*, a different namespace | Edit --container-md, not --breakpoint-md |
Frequently asked questions
Does Tailwind's container centre itself?
No. In both v3 and v4 the utility only sets width: 100% and per-breakpoint max-width. v3 let you opt into centring with theme.container.center: true; v4's CSS-first setup has no equivalent, so you add mx-auto at the call site or write your own rule.
How do I add padding to the container in Tailwind v4?
Either add px-4 next to container in the markup, or define your own .container in plain CSS with padding-inline. The second scales better if the gutter changes at more than one breakpoint, which is exactly why this codebase went that way: its gutter is 1rem, then 4rem at 1440px, then 3rem at 1536px.
Is container the same as max-w-7xl mx-auto?
Close, but not identical. max-w-7xl mx-auto is one fixed cap (80rem) at every width; container steps the cap at each breakpoint, so the column matches the breakpoint it is currently in. Use max-w-* for a reading column, container for a page shell.
Should I use the container utility or write my own?
Use the utility when the defaults fit and your breakpoints are Tailwind's. Define your own with @utility container when you have custom breakpoints you do not want as container tiers, or when the gutter and the cap change at different widths. Both cases applied here. Only shadow it with an unlayered .container rule if you are happy to ship the generated one alongside as dead CSS.
Where to take this next
The @theme block those breakpoints live in is the whole v4 configuration story — Tailwind v4 config covers what moved out of JavaScript and what replaced it, and the v4 migration guide walks the upgrade that removes tailwind.config.js in the first place. For the design tokens that sit beside these breakpoints in the same block, see Tailwind design tokens.
Templates in this post
ASoc Till, ASoc Timbre and ASoc Uptime all ship with this container rule already wired into a Container atom, so page width is one file rather than a class repeated across every section.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
