Sizing Media with min-content and max-content

Set width: max-content on a <figure> that wraps a full-resolution image and the box balloons to the image’s natural pixel width — overflowing the viewport and triggering a horizontal scrollbar — because the layout engine resolves the intrinsic-size contribution of a replaced element during the layout phase, before any available-space constraint is applied. This guide is part of Intrinsic Sizing and Aspect Ratio, which sits inside Layout and Paint Optimization; the failure it describes lives entirely in the layout phase, where min-content and max-content contributions are computed for every box in the containing block chain.

The min-content and max-content keywords are not decorative sizing shortcuts. They are direct handles on the browser’s intrinsic sizing algorithm — the same algorithm that governs table-cell shrink, flex-basis resolution, and grid track sizing. Reach for them on media and you are asking the engine to size the box from the content outward instead of from the container inward, which inverts the normal responsive contract and is a frequent, hard-to-spot source of both overflow and cumulative layout shift.

The symptom, minimally reproduced

Here is the smallest reproduction that makes the box misbehave. A figure holds a 2400px-wide photograph and a short caption. The intent is “shrink to fit the caption or the image, whichever is natural.” The result is a box wider than most phones.

<figure class="card">
  <img src="/media/skyline-2400.jpg" alt="City skyline at dusk" />
  <figcaption>Downtown, 2024</figcaption>
</figure>

<style>
.card {
  width: max-content; /* BAD: max-content of a replaced child = its 2400px intrinsic width */
  margin-inline: auto;
}
.card img { display: block; }
</style>

Because the image is a replaced element, its max-content contribution equals its intrinsic width — 2400px — regardless of the viewport. The figure inherits that as its preferred size, and nothing in this rule set clamps it back down. On a 390px viewport the card is six times too wide.

How the layout engine computes intrinsic contributions

To size any box with an intrinsic keyword, the layout engine walks the box’s subtree and asks each child for two numbers: its min-content contribution (the narrowest the child can be without overflowing its own content — for text, the longest unbreakable word) and its max-content contribution (the width the child would take with unlimited space and no wrapping). These contributions bubble up the box tree and are cached on each layout object so sibling passes can reuse them. This is the same read that a geometry access can trigger synchronously — see Forced Synchronous Layouts for how a mistimed offsetWidth forces this whole traversal to run mid-task.

For a replaced element<img>, <video>, <canvas>, or an inline SVG with intrinsic dimensions — both contributions collapse to the same value: the element’s intrinsic width (its natural pixel width, or the width implied by its intrinsic aspect ratio and a specified height). There is no wrapping to consider, so min-content and max-content are identical. This is the crux: min-content and max-content on a container behave very differently when the container holds text versus when it holds media.

Intrinsic-size contributions bubbling up the box tree A figure box queries an image child and a caption child for their min-content and max-content contributions; the replaced image returns identical values while the text caption returns a narrow min and a wider max. Layout phase: intrinsic contribution query figure (LayoutObject) width: max-content img (replaced) min-content = 2400px max-content = 2400px figcaption (text) min-content = 88px max-content = 210px figure max-content = max(child contributions) = 2400px → overflow no available-space clamp has been applied yet

When the figure asks for its own max-content, it takes the maximum of its children’s max-content contributions plus its own padding and borders. The image’s 2400px dominates the 210px caption, so the figure resolves to 2400px. The available space of the containing block never enters this computation — that clamp happens in a later step that max-content deliberately opts out of.

Two implementation details make this worse than it first appears. First, the intrinsic contribution of a replaced element is derived from its default object size and any specified width/height attributes or aspect-ratio, not from the rendered box — so lazy-loaded or not-yet-decoded images still contribute their declared intrinsic width the instant the layout object is created. Second, Blink caches min/max preferred widths on the layout object but must invalidate that cache up the entire ancestor chain when a descendant’s intrinsic size changes; a container sized by an intrinsic keyword therefore participates in more cache invalidations than a container with a definite width, which is why these boxes show up disproportionately in long Layout events. The takeaway for anyone reading a flame chart: an intrinsic keyword on a container is a standing invitation for the layout engine to re-query a whole subtree.

Why max-content overflows and min-content collapses

The two keywords fail in opposite directions, and knowing which one you reached for tells you which bug you will see. max-content refuses to wrap, so on media it pins the box to the largest intrinsic dimension in the subtree and overflows. min-content shrinks the box to the narrowest content that will not overflow its own children; for a replaced image that is still the full intrinsic width (an image cannot be narrower than itself without an explicit override), while for the surrounding text it collapses to the longest single word. Mix media and text in one box and min-content produces a box as wide as the image but with the caption jammed into a single-word column.

max-content overflow versus min-content collapse on the same figure A side-by-side comparison: max-content overflows the viewport frame, min-content stays as wide as the image because a replaced element cannot shrink below its intrinsic width. width: max-content width: min-content 390px viewport frame figure = 2400px (clipped) horizontal scrollbar appears 390px viewport frame figure still = 2400px img cannot shrink below intrinsic caption collapses to one word

The layout cost is not just visual. Because the intrinsic keyword forces the engine to resolve contributions from the full subtree before it can size the box, a page full of such figures pays for a deeper, less-cacheable layout traversal than one that sizes boxes from a definite width. The table below maps the three sizing strategies to their phase constraint and cost.

Pipeline phase Sizing input Cost
Layout — definite width (width: 100%) container available space Cheapest: box sized top-down, no subtree query
Layout — max-content / min-content subtree intrinsic contributions Full subtree traversal per box; overflow or collapse risk
Layout — fit-content(limit) min(max-content, max(min-content, limit)) Subtree query, but clamped — the correct middle path

The behaviour of these keywords is defined by the CSS intrinsic sizing model, the same model that decides which CSS properties trigger reflow versus repaint when you mutate them at runtime.

Reading it in a DevTools trace

Intrinsic-keyword overflow rarely shows up as a single expensive event; it shows up as a Layout event whose scope is the whole document because an over-wide box widened its containing block. In the Chrome performance panel, expand the Layout node under the offending frame and look at the Layout tree size and the Nodes that need layout count. Below, the annotated tree shows the tell-tale shape: a layout invalidated at the figure, propagating up to the document element, with a nodes-needing-layout count far larger than the DOM you actually changed.

Frame 41  (presented 22.8ms late)
└─ Task  main-thread  19.4ms
   └─ Layout  17.1ms                          ← single event, whole-document scope
      ├─ invalidated by: figure.card          ← width:max-content resolved to 2400px
      ├─ Layout tree size .......... 3,412 nodes
      ├─ Nodes that need layout .... 3,410 / 3,412   ← nearly the entire tree relaid out
      ├─ intrinsic-size query ...... img (2400×1600, replaced)
      │     min-content = 2400px
      │     max-content = 2400px               ← identical: replaced element
      └─ containing-block widened → <html> overflow-x → scrollbar layout → re-layout

The signature is the mismatch between the size of the DOM change (you added one figure) and the Nodes that need layout count (almost the whole tree). Whenever an intrinsic keyword lets a box exceed its container, the container’s own size becomes indefinite-then-definite in a way that invalidates siblings, and the layout cost scales with document size rather than with the change. If you also see a Layout Shift entry attached to the same frame, confirm it against debugging CLS with the Layout Instability API — the reflow that widens the document often nudges every box below it.

The fix: clamp intrinsic sizing with fit-content and aspect-ratio

The correct tool is almost never bare max-content on media. It is fit-content() — which computes min(max-content, max(min-content, limit)), giving you shrink-to-content behaviour that still respects an upper bound — combined with a definite max-width on the image itself and an explicit aspect-ratio so the box reserves its final height before the pixels decode. Reserving that height is what keeps the image out of the layout-shift budget, the same discipline covered in reducing layout shift from web fonts for text.

Decision path for sizing a media box A decision tree: if the box must never exceed the viewport use fit-content with a limit; if it should always fill use a definite width; reserve height with aspect-ratio in both cases. Sizing a media box? shrink to content, fit-content(60ch) + max-width always fill container, width: 100% + max-width variable width edge to edge both: set aspect-ratio to reserve height

Here is the complete before/after. The before is the broken reproduction; the after keeps the shrink-to-caption intent without ever exceeding the viewport, and reserves height so no shift lands in the frame.

<!-- BEFORE: max-content leaks the image's 2400px intrinsic width -->
<style>
.card {
  width: max-content;        /* BAD: replaced child pins figure to 2400px, overflows */
  margin-inline: auto;
}
.card img { display: block; }
</style>

<!-- AFTER: fit-content clamps the intrinsic query; the image carries a definite ceiling -->
<style>
.card {
  width: fit-content(60ch);  /* min(max-content, max(min-content, 60ch)) — never runs away */
  max-width: 100%;           /* hard clamp to containing block; kills overflow entirely */
  margin-inline: auto;
}
.card img {
  display: block;
  max-width: 100%;           /* image shrinks with the box instead of forcing its width */
  height: auto;
  aspect-ratio: 3 / 2;       /* reserves final box height pre-decode: no layout shift */
}
</style>

The fit-content(60ch) keeps the figure hugging its caption up to a 60ch limit, max-width: 100% guarantees the box can never exceed its containing block, and max-width: 100% on the image lets the replaced element participate in the shrink instead of dictating it. The aspect-ratio reserves the layout box’s height from the first layout pass, so the image decoding later does not re-run layout on the subtree — the same isolation goal you get from scoping reflow with CSS containment strategies.

One caveat worth internalizing: fit-content() with a length argument still runs the intrinsic-size query — it is a clamp on the result, not an escape from the traversal. If a box will always fill its container, prefer a plain definite width: 100%, which lets the engine size top-down and skip the subtree query entirely. Reserve the intrinsic keywords for the genuine case where the box must be exactly as wide as its content and no wider, such as a caption block or a tightly wrapped inline-badge. Applying that rule mechanically — definite width when you can, fit-content with a hard limit when you must shrink, aspect-ratio on every media box either way — removes both failure modes without a single line of JavaScript measuring the box, which is the outcome you want: geometry resolved once, in the layout phase, with no read forcing a synchronous flush.

Verification checklist

Frequently Asked Questions

Do min-content and max-content produce different sizes on an image?

No. For a replaced element such as an <img> or <video> with intrinsic dimensions, both keywords resolve to the same value — the element’s intrinsic width. Wrapping is what makes the two differ, and a replaced element has no wrapping to consider. The keywords only diverge when the box’s widest child is text or another shrinkable, wrappable box.

Why does max-content cause a horizontal scrollbar but width 100 percent does not?

max-content sizes the box from its content outward and deliberately ignores the available space of the containing block, so a 2400px image forces a 2400px box regardless of viewport. width: 100% sizes the box from the container inward, so it can never exceed what the container offers. The intrinsic keyword opts out of the very clamp that prevents overflow.

Is fit-content always better than max-content for media?

For media, almost always. fit-content(limit) computes min(max-content, max(min-content, limit)), giving you the shrink-to-content behaviour people usually want from max-content while capping the result at a sane limit. Bare max-content is only appropriate when you have already guaranteed the content can never be wider than the viewport.

Does setting aspect-ratio remove the intrinsic width problem on its own?

No. aspect-ratio reserves height and prevents layout shift once a width is known, but it does not clamp width. You still need max-width: 100% on the image and a bounded width on the container. Aspect-ratio solves the vertical shift; the intrinsic keyword clamp solves the horizontal overflow.

How do I confirm the fix reduced layout cost in a trace?

Record a performance profile and expand the Layout event for the frame that inserts or resizes the media. Before the fix the Nodes that need layout count approaches the full layout tree size because the over-wide box invalidates the document; after the fix that count should be confined to the figure’s own subtree, and no Layout Shift entry should attach to the frame.