How to Reserve Space for Embeds with aspect-ratio
This walkthrough belongs to Reducing Cumulative Layout Shift (CLS) inside Core Web Vitals & Measurement. An <iframe> with no dimensions renders as a 300×150 box, and then jumps to its real size when the embedded document loads. Everything below it moves. On an article page with three embeds, that pattern alone can spend the entire 0.1 CLS budget before a single image is considered.
The fix is to make the box correct before the content arrives, using aspect-ratio where the shape is known and a measured min-height where it is not.
Rapid Diagnosis: Find the Boxes That Change Size
- Turn on Layout Shift Regions in the Rendering panel and reload. Every embed that resizes will flash a highlight over itself and everything beneath it.
- Read the shift sources. In the Performance panel's Experience track, each shift record names the element that moved — an
<iframe>or its wrapper is the giveaway. - Compare initial and final geometry. Select the embed in the Elements panel before and after it loads; a change in computed height is your shift.
- Check whether the embed resizes itself later. Some widgets post a height message to the parent and grow after their content renders — a second shift, well after load.
Root Cause Analysis
1. Iframes have a default size. With no width/height attributes and no CSS, an iframe is 300×150. Whatever the embedded content turns out to be, the initial layout is wrong and correcting it is a shift.
2. Percentage widths do not imply a height. width: 100% with no height still leaves the browser guessing, so a responsive embed resizes vertically even when its width was correct from the start.
3. Self-resizing widgets shift twice. A widget that measures its content and posts a new height to the parent produces a second shift long after load — and because it is not within 500ms of a user interaction, it counts in full.
4. Variable-height content genuinely has no known ratio. A comment thread or a social post can be any height. There is no ratio to reserve, so the solution has to be a sensible minimum plus a scroll container, not a fixed aspect.
Step-by-Step Resolution
1. Use aspect-ratio for anything with a known shape. Video, maps and most media embeds have a fixed ratio.
.embed {
width: 100%;
aspect-ratio: 16 / 9; /* the box exists before the iframe loads */
border: 0;
display: block;
}
<iframe class="embed" src="https://player.example.com/v/abc" title="Product demo"
loading="lazy" allowfullscreen></iframe>
<!-- trade-off: aspect-ratio guarantees no shift only if the ratio is right.
A 4:3 source inside a 16:9 box letterboxes rather than shifts — visually
imperfect but metrically clean. Match the ratio to the source. -->
Expected outcome: the embed's contribution to CLS drops to zero, regardless of when it loads.
2. Keep the width and height attributes as well. They give the browser the ratio even before CSS is applied, which matters if your stylesheet is deferred.
<iframe src="..." width="1280" height="720" style="width:100%;height:auto"
title="Product demo" loading="lazy"></iframe>
<!-- trade-off: the inline style is needed because the attributes alone set a
fixed pixel size. Attributes for the ratio, CSS for the responsiveness. -->
3. For unknown-height embeds, reserve a measured minimum. Take the median rendered height from real pages rather than guessing.
.embed-social {
width: 100%;
min-height: 480px; /* p50 rendered height, measured across 50 posts */
contain: layout; /* changes inside cannot reflow the page around it */
}
/* trade-off: a minimum that is too generous leaves visible whitespace under
short embeds; too small and tall ones still shift. Measure, then round up
slightly — whitespace is cheaper than a layout shift. */
Expected outcome: most instances never resize; the remainder shift by a much smaller distance, so the CLS contribution falls by roughly the ratio of the error.
4. Handle self-resizing widgets explicitly. If a widget posts its height, apply it before first paint where possible, or accept the first size and disallow later growth.
addEventListener('message', (e) => {
if (e.origin !== 'https://widget.example.com') return;
const frame = document.querySelector('#widget');
if (!frame || typeof e.data?.height !== 'number') return;
// Only ever grow the reserved box if the widget is off-screen.
const onScreen = frame.getBoundingClientRect().top < innerHeight;
if (!onScreen) frame.style.height = `${e.data.height}px`;
}, false);
// trade-off: refusing to resize an on-screen widget can clip its content.
// Pair it with overflow scrolling inside the reserved box so nothing is lost —
// a scrollbar is a better outcome than moving the article the user is reading.
Verification
Reload with Layout Shift Regions enabled and confirm no highlight appears over or below any embed, on both a phone-sized and a desktop-sized viewport — a ratio that works at one width can be wrong at another if the embed has a fixed minimum.
Quantify the change with the same observer you use for other shift work:
let cls = 0;
new PerformanceObserver((l) => {
for (const e of l.getEntries()) {
if (e.hadRecentInput) continue;
cls += e.value;
for (const s of e.sources ?? []) {
if (s.node?.tagName === 'IFRAME') console.log('iframe shift', e.value.toFixed(4));
}
}
}).observe({ type: 'layout-shift', buffered: true });
Then keep it from regressing. Embeds are usually added by editors rather than engineers, so the guard belongs in the content pipeline: reject or auto-wrap any embed markup that lacks dimensions, and assert a CLS budget in CI so a template change cannot quietly reintroduce the problem.
FAQ
Is the padding-top percentage hack still needed?
No. aspect-ratio is supported across all current browsers and expresses the intent directly, without the wrapper element and absolute positioning the old technique required. Keep the hack only if you must support a legacy engine, and then apply it as a fallback beneath the modern declaration rather than instead of it.
Does lazy-loading an iframe cause layout shift?
Not by itself — provided the box is reserved. loading="lazy" changes when the embedded document loads, not how much space it occupies, so an embed with a correct aspect-ratio will never shift regardless of load timing. Lazy loading an unreserved embed makes things worse, because the resize now happens during scrolling, when it is most noticeable.
What about embeds whose height depends on the viewport?
Use a ratio that is itself responsive: a media query can set aspect-ratio: 4 / 3 on narrow screens and 16 / 9 on wide ones, and because the value is applied before load there is still no shift. What you must avoid is switching the ratio after the embed has rendered, which is a shift you created yourself.
How do I reserve space for content whose height I genuinely cannot predict?
Give it a measured minimum and let it scroll inside that box rather than growing the page. A comment thread or a feed can be given the median rendered height as min-height and overflow-y: auto, so late content changes what is inside the box without changing the box. That is a deliberate design decision — a scrollbar inside a section is a visible cost — but it is a smaller cost than moving the article a reader is part-way through.
Does content-visibility interact with reserved space?
Yes, and the pairing matters. content-visibility: auto skips rendering off-screen sections, which is a real rendering win on long pages, but the browser then needs contain-intrinsic-size to know how much room to leave. Without it the skipped section collapses and the scrollbar jumps as the user scrolls — the same shift problem in a different costume. Always set an intrinsic size alongside it.
Should I reserve space for content that only some users see?
Reserve it only if it renders above the fold, and prefer not rendering it at all otherwise. A consent banner, a promotional strip or a personalised message that appears after a check will shift everything below it if it is injected into the flow. The stable options are to render it in a fixed overlay outside the document flow, to reserve its box unconditionally and accept the whitespace, or to decide server-side so it is present in the first paint.
Related
- Reducing Cumulative Layout Shift (CLS) — the full shift taxonomy this case belongs to.
- Debugging CLS caused by dynamic ad injection — the harder version of the same problem, where the size is unknown by design.
- Replacing YouTube embeds with a facade — remove the embed's cost entirely while keeping the reserved box.