Skip to main content
ASoc
Tutorial

Tailwind Max Width: 62 Usages, 57 Arbitrary, and 96 Lost Pixels

max-w-* reads the --container-* scale in v4, and max-w-md is 448px, not 768px. A census of 62 usages here, plus the viewport band where our layout narrows as it widens.

The ASoc Team12 min read

max-w-* sets a CSS max-width. In Tailwind v4 the named scale — max-w-xs through max-w-7xl — is not its own scale at all: it reads the --container-* theme variables, so max-w-3xl compiles to max-width: var(--container-3xl), which is 48rem. Arbitrary values use bracket syntax: max-w-[870px].

This storefront uses max-w-* 62 times across 34 files. Fifty-seven of those are arbitrary pixel values. That ratio looks like a smell and mostly is not one, for a reason worth explaining — and the audit that produced it found a 96-pixel band where the layout gets narrower as the window gets wider.

The v4 scale, measured rather than remembered

The named rungs come straight out of node_modules/tailwindcss/theme.css. Compiling max-w-3xl against tailwindcss 4.3.1 emits:

.max-w-3xl {
  max-width: var(--container-3xl);
}

The full scale, and the trap in the middle of it:

UtilityTokenValuepx @ 16px root
max-w-3xs--container-3xs16rem256
max-w-2xs--container-2xs18rem288
max-w-xs--container-xs20rem320
max-w-sm--container-sm24rem384
max-w-md--container-md28rem448
max-w-lg--container-lg32rem512
max-w-xl--container-xl36rem576
max-w-2xl--container-2xl42rem672
max-w-3xl--container-3xl48rem768
max-w-4xl--container-4xl56rem896
max-w-5xl--container-5xl64rem1024
max-w-6xl--container-6xl72rem1152
max-w-7xl--container-7xl80rem1280

max-w-md is 448px, not 768px. It shares a name with the md breakpoint and nothing else — the md breakpoint is 48rem/768px, which is max-w-3xl. Two different scales, five overlapping labels. This is the single most common max-w mistake, and the fact that both scales exist in the same @theme block is why.

Because they are ordinary theme variables, redeclaring one in your own @theme moves every max-w-*, w-* and min-w-* that references it. That is a feature when you want a design-wide measure, and a surprise when you only meant to change one card.

max-w-screen-* was not deleted in v4

Widely repeated, and wrong as of 4.3.1. Compiling max-w-screen-md against the installed version still produces output:

.max-w-screen-md {
  max-width: var(--breakpoint-md);
}

It survives as a deprecated compatibility utility pointing at the breakpoint token. The forward spelling for the same intent is the arbitrary-property syntax, which reads the token directly and does not depend on a deprecation surviving another minor:

<div class="max-w-(--breakpoint-md)">…</div>

If you have max-w-screen-* in a v4 codebase it is not broken today. It is a rename you can do on your own schedule rather than an outage.

The census: 62 usages, 57 arbitrary

Run against src/, excluding the article prose in src/content/blog/:

$ grep -rEo "max-w-[A-Za-z0-9._%/-]+|max-w-\[[^]]+\]" src \
    --include=*.tsx --include=*.ts --include=*.css \
    | grep -v '^src/content/blog' > /tmp/maxw.txt

$ wc -l < /tmp/maxw.txt
62

$ cut -d: -f2- /tmp/maxw.txt | sort | uniq -c | sort -rn | head -7
      8 max-w-[870px]
      6 max-w-[570px]
      5 max-w-[760px]
      3 max-w-[880px]
      3 max-w-[770px]
      3 max-w-[720px]
      3 max-w-[1060px]

Five named utilities in the whole codebase — max-w-xs, max-w-sm, max-w-3xl, max-w-none, max-w-max — against 57 arbitrary values covering 28 distinct pixel numbers and one percentage. Zero max-w-screen-*. Zero max-w-full.

The instinct is to call that a token failure and map everything onto the named scale. Look at where the numbers land before you do:

Design valueremNearest rung aboveNearest rung below
870px54.375max-w-4xl (896)max-w-3xl (768)
760px47.5max-w-3xl (768)max-w-2xl (672)
570px35.625max-w-xl (576)max-w-lg (512)

The --container-* scale is modular — it steps 16, 18, 20, 24, 28, 32, 36, 42, 48, 56, 64, 72, 80rem, roughly a quarter at a time near the top. A design's measure is not a modular quantity; it is "how many characters fit on a line before reading gets worse," and it lands wherever it lands. Snapping 870 to 896 to win a nicer class name changes the rendered design to make the CSS tidier. That is the wrong trade.

Where the census does show a real problem is the repetition. max-w-[870px] appearing eight times is a design token that was never declared. The fix is one line in @theme:

@theme {
  --container-section: 870px;
  --container-prose: 760px;
}

…which makes max-w-section and max-w-prose real utilities, at the design's own values, with no rounding. In v4 you do not choose between the named scale and the true number. You name the true number.

The two jobs max-w does, and why this codebase keeps them apart

Every section in this repository composes the same two layers:

// src/components/organisms/ArticleBody.tsx
<section className="pb-16 md:pb-20">
  <Container>
    <div className="mx-auto w-full max-w-[760px]">{children}</div>
  </Container>
</section>

Container (src/components/atoms/Container.tsx) is the page shell — how wide the site is allowed to get, plus the gutter. The inner max-w-[760px] is the measure — how wide this particular content should be for reading. The shell is global and appears once; the measure is local and differs per section. Thirty-one files render Container; all 57 arbitrary values live inside one.

Keeping them separate is why max-w-[870px] can repeat eight times without being a bug: it is the same editorial decision (a centred section header) reached in eight sections, not eight copies of a layout constant. w-full beside it matters too — max-width alone does not make a block fill the space below the cap in every flex or grid context, and mx-auto is what actually centres it.

The defect: a 96px band where wider means narrower

The page shell is a hand-written rule in src/app/globals.css, not Tailwind's container utility:

.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; } }

Read the 1440 tier carefully. It raises the gutter to 4rem and sets no max-width, so the cap stays at the 1280px inherited from the tier below. Preflight puts everything in box-sizing: border-box, so padding comes out of that cap. The usable content width:

Viewportmax-widthGutter (each side)Content width
1280–14391280px16px1248px
1440–15351280px (inherited)64px1152px
≥ 15361536px48px1440px

Content width goes 1248 → 1152 → 1440. Drag a 1439px window one pixel wider and the content column loses 96 pixels, 7.7% of itself. It is not a rendering bug; it is arithmetic, and it is invisible in every screenshot because nobody screenshots two viewports 1px apart.

The consequence is that two inner constraints go dead in that band. max-w-[1170px] in FeatureTabs.tsx and max-w-[1200px] in src/app/dashboard/layout.tsx both bind at 1280–1439 (1248px of room) and again at ≥1536 (1440px of room), but between 1440 and 1535 the shell is 1152px — narrower than either cap. The declared widths are unreachable there, so the code says one thing and the layout does another.

Either the 1440 tier lost a max-width line, or the padding jump is deliberate breathing room. We have not changed it in this post, because altering the shell width silently re-lays-out all 336 prerendered routes of this site and that is not a blog-post-sized edit. It is written down here so the next person reading FeatureTabs.tsx knows why the number they see is not the number that renders. The one-line version, if you hit the same shape:

@media (min-width: 1440px) { .container { max-width: 1440px; padding-inline: 4rem; } }

The other finding: 393 bytes of dead container CSS

Because the override is a plain .container rule while Tailwind's is generated inside @layer utilities, the compiled stylesheet ships both. From the real production build:

Tailwind's container utility ... 393 bytes
the globals.css override ....... 463 bytes
total stylesheet ............... 73,464 bytes

Walking the compiled file confirms the split: Tailwind's rule sits inside @layer utilities, the override sits in no layer at all. Unlayered normal declarations outrank layered ones whatever the source order, so all 393 bytes of the generated utility are unreachable — 0.5% of the CSS, downloaded on every page, and eight media-query blocks a maintainer has to disambiguate before they can trust the other eight.

There is a second-order effect worth knowing about. Tailwind's container reads --breakpoint-*, and this repo declares two custom ones (--breakpoint-2xsm: 375px, --breakpoint-xsm: 425px). The generated utility grew tiers for both:

.container{width:100%}
@media (min-width:375px){.container{max-width:375px}}
@media (min-width:425px){.container{max-width:425px}}
@media (min-width:40rem){.container{max-width:40rem}}
…

Adding a breakpoint token in v4 widens the container utility as a side effect. In a codebase that actually uses container, that is a layout change nobody wrote. The clean fix is to stop shadowing the class and define it once, which is exactly what @utility is for:

@utility container {
  width: 100%;
  margin-inline: auto;
  padding-inline: 1rem;
  /* …the project's own tiers, and no duplicate */
}

Mistakes and how they show up

MistakeHow it shows upFix
Reading max-w-md as the md breakpointAn element caps at 448px when you expected 768pxmax-w-3xl is 48rem; the scales only share labels
max-w-* with no mx-autoCorrect width, pinned left, "the centring is broken"max-width caps; margin-inline: auto centres
max-w-* with no w-fullBlock collapses to content width in a flex parentPair them: w-full max-w-[760px]
Snapping design values to the named scaleThe rendered design shifts to make classes prettierDeclare --container-<name> at the true value
An inner cap wider than the shell's content boxThe declared number never renders; nothing errorsCheck the shell's max-width minus its padding
Raising a container's padding without its max-widthContent narrows as the viewport widensSet both in the same tier
Shadowing .container with a plain ruleTwo rule sets ship; one is dead bytesRedefine it with @utility container
Assuming max-w-screen-* was removed in v4A migration nobody needed to do this weekIt still compiles, to var(--breakpoint-*)

Frequently asked questions

What is the difference between w- and max-w- in Tailwind? w- sets width — a demand. max-w- sets max-width — a ceiling that only takes effect when the available space exceeds it. Responsive layouts almost always want the ceiling, because the element should shrink freely on a phone and stop growing on a desktop. w-full max-w-[760px] is the canonical pair: take all the width you can get, up to 760px.

How do I set a custom max width in Tailwind? Two ways, and they are not equivalent. One-off: bracket syntax, max-w-[870px], which compiles a single utility. Repeated: declare --container-<name> in your @theme block and use max-w-<name>, which gives the value a name, one place to change it, and the same value to w- and min-w-. Our census says the switch point is roughly the third occurrence — max-w-[870px] appearing eight times is a token that should have been declared.

Is max-w-full the same as w-full? No. max-w-full is max-width: 100%, which stops an element overflowing its parent but does not make it fill anything — it is the standard guard on images and embeds. w-full is width: 100%, a demand to fill. This codebase uses max-w-full zero times because the images are already constrained by their containers.

Why is my max-w-* being ignored? Usually one of three things. The parent is narrower than your cap, so the cap never binds — the case we found at 1440px above, where a 1152px shell makes a max-w-[1200px] child unreachable. Or the element is a flex/grid item whose flex-basis or track sizing is deciding its width. Or another rule wins the cascade: an unlayered plain-CSS rule beats a @layer utilities one regardless of source order, which is precisely how the .container override in this repo overrides Tailwind's.

Should the page shell use Tailwind's container or a custom rule? Use Tailwind's, customised through @utility container, unless you need behaviour it cannot express. A parallel hand-written .container costs you a duplicate rule set in the bundle and a cascade puzzle for the next reader. It is also brittle in v4 specifically: the generated utility picks up every --breakpoint-* token you declare, so the two definitions drift apart as the theme grows.

Templates that ship this layout system

ASoc Timbre, ASoc Vox and ASoc Weave are built on the same two-layer arrangement measured here — a responsive shell every section renders inside, with per-section measure caps set at the design's real values rather than snapped to a scale.

Browse the sets: Next.js landing page templates and Tailwind landing page templates. The breakpoint half of this token block is audited in Tailwind Breakpoints: 325 Variants, Two Dead Ones; when the correct width depends on the element's slot rather than the window, the container-query audit covers the inversion this post's shell arithmetic hints at. The type half of the same @theme block is audited in Tailwind font-family, including why --font-sans has to be fed from outside Tailwind.

Keep reading

Tutorial11 min read

Scroll-Driven Animations in Tailwind v4 Without a JS Library

animation-timeline replaces the scroll-animation library category — behind two guards. The Tailwind v4 setup, the element you must never fade in, and when IntersectionObserver still wins.

Read more
Tutorial11 min read

Multi-Tenant Theming with Tailwind CSS v4 and CSS Variables

Tailwind v4 tokens compile to real CSS custom properties, so one build can serve every tenant's brand. The override pattern, contrast handling, and the pitfalls.

Read more