Intrinsic Sizing and Aspect Ratio

An <img> or <iframe> that arrives on the page with no declared dimensions occupies zero height until its bytes land. The moment the resource decodes, the browser learns its natural size, expands the box, and pushes every element below it downward. That downward shove is a layout shift — the single most common cause of a poor Cumulative Layout Shift score, and one of the most avoidable. The fix is to tell the layout engine the final geometry before the pixels exist, so the box is already the right size when content flows into it.

This guide is part of Layout and Paint Optimization, which covers the layout and paint phases where geometry is resolved and dirty regions are rasterized. Reserving space is the front line of that work: a box that never resizes never triggers a second reflow, and a subtree that never reflows never repaints. Here we cover intrinsic versus extrinsic sizing, the aspect-ratio property, the width and height attributes on images, the min-content/max-content keywords, and how missing dimensions cascade into reflow. We diagnose it all with the Layout Instability API and Chrome DevTools.

Diagnosing Layout Shift from Unsized Media

Layout shift from media has a distinctive signature: it happens after first paint, correlated with network completion rather than with user input. Before touching any CSS, confirm the diagnosis. Walk this checklist inside DevTools:

  • Open the Performance panel, record a page load with CPU: 4x slowdown and Network: Slow 4G, and look for red Layout Shift markers in the Experience track. Each marker is clickable and reports the shift score and the moving nodes.
  • In the Rendering drawer (Command Menu → Show Rendering), enable Layout Shift Regions. Shifted areas flash blue on the live page — reload and watch where images land.
  • In the Elements panel, select a suspect <img> and check the Computed pane. If aspect-ratio reads auto and there is no width/height attribute, the box has no reserved geometry.
  • In the Network panel, sort by type Img. Any image whose row finishes well after DOMContentLoaded is a candidate to shove content that has already painted.
  • Read the CLS number in the Performance insights panel or a Core Web Vitals measurement harness. Field CLS above 0.1 with no interaction almost always means unsized media.

The programmatic version uses the Layout Instability API through a PerformanceObserver. Each layout-shift entry carries a value, a hadRecentInput flag, and the sources array naming the nodes that moved:

// Attribute every shift to the DOM node that moved — run this in the field
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.hadRecentInput) continue;           // ignore user-driven shifts
    for (const src of entry.sources) {
      console.log(entry.value.toFixed(4), src.node); // src.node is the shifted element
    }
  }
}).observe({ type: "layout-shift", buffered: true }); // buffered replays pre-observer shifts
Layout shift timeline for an unsized image A timeline shows first paint, an image decode landing later, the resulting downward shift of text, and the accumulating layout-shift score. Frame timeline: unsized img decodes after first paint time First Paint Image decode lands Content reflowed down box height = 0 box grows to 480px shift score += 0.18 CLS = 0.18 (poor)

If the shifting nodes are text blocks or cards sitting directly beneath an image, embed, or ad slot, you have confirmed the pattern. The layout engine did exactly what it was told — it just was not told enough, early enough.

Intrinsic vs Extrinsic Sizing: Where the Shift Comes From

The whole problem lives in the gap between two ways a box gets its size. Extrinsic sizing comes from the outside: a width: 300px rule, a percentage of the container, a flex basis. The layout engine can honor extrinsic sizes during the very first layout pass because the values are present in the CSS, before any resource loads. Intrinsic sizing comes from the content itself: an image’s natural pixel dimensions, the width of the longest word in a paragraph, the rendered size of a video frame. Intrinsic sizes are only knowable once the content is available — and for network resources, that is the moment the shift happens.

For a replaced element like <img>, <video>, or <iframe>, the intrinsic size is the resource’s own dimensions. With nothing else specified, the box collapses to a default (images to roughly 0 height before load, or a 300×150 default for some embeds) and then snaps to the intrinsic size on load. The three root causes below each break the same way — the engine cannot compute final geometry during first layout — and each has its own dedicated fix.

Extrinsic versus intrinsic sizing sources A comparison matrix contrasts extrinsic sizing sources known at first layout with intrinsic sizing sources known only after content loads. Extrinsic — known at first layout width: 300px / height: 200px aspect-ratio: 16 / 9 width / height HTML attributes flex-basis / grid track size no reflow when the resource lands Intrinsic — known after load image natural pixel dimensions decoded video frame size min-content / max-content iframe document layout forces reflow the instant it resolves

The first root cause is missing width and height attributes on raster images — the classic case that preventing image layout shift with aspect-ratio tackles directly. The second is fluid replaced elements sized only in one axis: an image with width: 100% but no ratio hint still collapses vertically. The third is container-driven intrinsic sizing — layouts that lean on min-content and max-content to fit media, where the wrong keyword forces the engine to measure content it does not yet have; sizing media with min-content and max-content works through those keywords in detail. All three converge on the deeper mechanism explored in avoiding reflow from intrinsic media dimensions: the second layout pass that fires when intrinsic geometry finally resolves.

Reserving Space Before Load: The Fix Procedure

The goal of every fix is identical — give the layout engine a computable box height during the first layout pass. There are two supported mechanisms, and modern browsers connect them: the width and height HTML attributes on an <img> are read by the UA stylesheet to synthesize an aspect-ratio, so a plain <img width="1600" height="900"> reserves the correct proportional height even when CSS stretches it to width: 100%. Follow this procedure.

  1. Read the resource’s real pixel dimensions. In the Network panel, hover the image row; the preview tooltip reports natural width and height. Never guess — a wrong ratio reserves the wrong box and you trade one shift for another.
  2. Add matching width and height attributes to the markup. These are unitless pixel counts, not CSS. They seed the UA aspect-ratio computation.
  3. Let CSS drive the display size with width: 100%; height: auto. Because the attributes already fixed the ratio, height: auto now resolves proportionally at first layout instead of collapsing to zero.
  4. For elements with no natural ratio<iframe> embeds, video players, ad slots — set aspect-ratio explicitly in CSS and wrap or size accordingly.
  5. Re-record in the Performance panel. The Layout Shift markers under the Experience track for that region should disappear. Confirm the CLS delta in Performance insights.
  6. Verify the box existed pre-load. Throttle to Slow 4G, reload, and watch the throbber region: the placeholder box should already occupy full height while the image is still downloading.
Decision tree for reserving media space A decision tree routes raster images to width and height attributes, ratio-known embeds to the aspect-ratio property, and unknown embeds to a padding wrapper. Media has no reserved box Is it a raster image? yes no Set width & height attrs UA synthesizes aspect-ratio Is the ratio known? yes no aspect-ratio: W / H in CSS height: auto follows the ratio padding-top hack measure, then fix

Before and after

The before case is the default markup a CMS or Markdown pipeline emits — an image with a src and nothing else. The after case reserves the box at parse time.

<!-- BEFORE: box height is 0 until decode; content below shifts on load -->
<img src="/hero.avif" alt="Product hero" class="hero">
<style>
  .hero { width: 100%; }        /* one axis only — vertical collapses to 0 */
</style>
<!-- AFTER: attributes seed a UA aspect-ratio; box height computes at first layout -->
<img src="/hero.avif" alt="Product hero" class="hero"
     width="1600" height="900">    <!-- unitless px, read by the UA stylesheet -->
<style>
  .hero {
    width: 100%;
    height: auto;                 /* resolves via synthesized aspect-ratio, not 0 */
    aspect-ratio: 1600 / 900;     /* belt-and-braces if attrs are ever stripped */
  }
</style>

For an embed with no intrinsic ratio, drive the whole box from CSS. The property replaces the old padding-top percentage trick, which forced a wrapper and an absolutely positioned child:

/* BEFORE: padding hack — needs a wrapper and abs-positioned iframe */
.embed-wrap { position: relative; padding-top: 56.25%; } /* 16:9 by hand */
.embed-wrap > iframe {
  position: absolute; inset: 0; width: 100%; height: 100%;
}

/* AFTER: aspect-ratio reserves the box directly, no wrapper, no reflow */
.embed {
  width: 100%;
  aspect-ratio: 16 / 9;   /* box height known at first layout, before load */
  border: 0;
}

The mechanism is the same in both cases: because the layout engine can compute a concrete height during the first layout pass, the resource loading later writes into an already-correct box and no second reflow — and therefore no repaint of the surrounding subtree — is scheduled.

Sizing Media with Intrinsic Keywords

Extrinsic pixel values are not always the right tool. Card grids, captions, and figures often need a box that hugs its content, and that is exactly what the min-content and max-content keywords express. max-content sizes a box to the widest its content wants to be with no wrapping — for an image, its intrinsic width; for text, the full unbroken string. min-content sizes it to the narrowest it can be without overflow — an image clamps to its intrinsic width (replaced elements do not shrink below it), while text collapses to its longest word. fit-content clamps max-content to the available space, giving a box that shrinks to fit but never overflows.

The performance trap is that these keywords ask the engine to measure content. If a grid track is grid-template-columns: max-content and the cell holds an unsized image, the track cannot resolve its width until the image decodes — reintroducing the very load-time reflow you were trying to kill. The rule of thumb: pair intrinsic keywords with media that already has a reserved box, so the measurement is available at first layout.

min-content, max-content, and fit-content widths Three horizontal bars compare how min-content collapses to the longest word, max-content expands to the full content width, and fit-content clamps to the container. Same content, three sizing keywords 0 container edge min longest word max full content, no wrapping (may overflow container) fit max-content clamped to the container edge replaced elements never shrink below their intrinsic width under min-content

A caption that should never be wider than its image, for instance, is a clean max-content use — but only once the image itself carries width/height, so the intrinsic width is known synchronously:

/* figure hugs the image; caption clamps to the image's intrinsic width */
figure { width: max-content; margin: 0; }
figcaption { width: 100%; }        /* inherits the resolved max-content width */
/* image MUST carry width/height attrs so max-content resolves at first layout */

Framework Edge Cases: React, Vue, and Next.js

Component frameworks add a second timing hazard on top of the network: hydration. Server-rendered markup may paint with a reserved box, then a client component re-renders and drops the width/height props, collapsing the box after paint — a shift the Performance panel attributes to script, not network.

  • React / JSX: width and height are honored as attributes, but only if you actually pass them. A <img> spread from a props object that omits them ships an unsized image. Enforce both, and prefer aspect-ratio in CSS as a fallback so a missing prop cannot collapse the box.
  • Next.js <Image>: the component requires width and height (or fill with a sized parent) precisely to reserve space — it emits an inline aspect-ratio wrapper. Bypassing it with a raw <img> forfeits that protection. With fill, the parent must be position: relative and have its own reserved height, or the fill target has nothing to fill.
  • Vue: static width/height attributes pass through fine, but a :style binding that computes size from reactive state can arrive a tick late. Keep the ratio in a plain CSS class, not a reactive binding, so it applies during the first layout rather than after the first reactive flush.
  • CSS-in-JS: styles injected at runtime (emotion, styled-components without SSR extraction) apply after first paint. If the aspect-ratio lives only in a runtime-injected rule, the box is unsized for the first frame. Extract critical sizing to static CSS or SSR the styles.

Reading state in a layout effect to size media is a related trap — measuring the DOM in useLayoutEffect and writing back a height forces a synchronous layout flush on every render. Declare the ratio in CSS and let the engine resolve it once.

Hydration timing and the second layout shift A sequence contrasts an SSR box that keeps its reserved height through hydration against a client re-render that drops the dimensions and shifts after paint. SSR paint → hydration → re-render SSR HTML paints Hydration ratio kept: no shift dims dropped: shift CLS += x Rule: keep the aspect-ratio in static CSS or SSR-extracted styles — never in a runtime-injected rule or a reactive :style binding that applies after first paint.

Metric Targets

Validate the fix against concrete numbers. Each row names the phase, the constraint that phase imposes, and the cost of getting it wrong.

Pipeline phase Constraint Cost if unmet
First layout Box height must be computable from CSS/attrs Box collapses to 0; reflow on decode
Load / decode No geometry change on resource arrival Layout shift, layout-shift entry emitted
Field CLS Session-window CLS ≤ 0.10 (good) 0.10–0.25 “needs improvement”, >0.25 poor
Per-shift value Individual layout-shift value ≈ 0 Any non-zero value near media = unsized box
Hydration Reserved box survives client re-render Post-paint shift attributed to script

Measurement method: record field CLS through a Core Web Vitals harness and attribute individual shifts with the Layout Instability API via a PerformanceObserver. A passing trace shows zero Layout Shift markers under the Experience track for every media region across a throttled reload. Font swaps can also inflate CLS independently of media — if text is jumping rather than images, see reducing layout shift from web fonts.

In This Topic

Frequently Asked Questions

Do width and height attributes on an image override my CSS width?

No. Modern browsers read the width and height attributes only to compute a default aspect-ratio for the element. Your CSS width: 100%; height: auto still controls the displayed size — the attributes just supply the ratio so height: auto resolves proportionally at first layout instead of collapsing to zero. Keep both the attributes and the CSS.

Should I use aspect-ratio or the padding-top percentage hack?

Use aspect-ratio for anything that supports it. The padding-top percentage trick required a wrapper element and an absolutely positioned child, and it could not respond to intrinsic content. The aspect-ratio property reserves the box directly on the element with no extra markup. Reserve the padding hack only as a fallback for very old browsers you still support.

Why does my image still shift even though I set aspect-ratio?

Common causes: the rule is injected by runtime CSS-in-JS and applies after first paint; a framework re-render drops the sizing props during hydration; or the ratio you set does not match the resource’s real dimensions, so the box resizes on decode. Check the resource’s natural size in the Network panel and confirm the ratio is in static or SSR-extracted CSS.

Does max-content cause layout shift?

It can, indirectly. max-content asks the engine to measure the content’s natural size. If that content is an unsized image, the measurement is not available until the image decodes, so the track resolves late and content around it reflows. Pair intrinsic keywords with media that already carries width/height attributes so the measurement is available at first layout.

How do I attribute a layout shift to a specific element?

Use the Layout Instability API through a PerformanceObserver. Each layout-shift entry exposes a sources array whose entries carry a node reference to the element that moved, plus its previous and current rectangles. Filter out entries where hadRecentInput is true to ignore user-driven shifts, then log source.node for each remaining entry.