How Images and Media Cause Layout Shift (and How to Fix It)
The complete guide to preventing CLS from images, videos, iframes, responsive images, and media embeds

How Images and Media Cause Layout Shift (and How to Fix It)
The 2025 Web Almanac puts a number on what I keep seeing in the field: 62% of mobile pages have at least one image without explicit width and height. That makes unsized media the number one cause of Cumulative Layout Shift on the web. And every one of these shifts is preventable with techniques that have existed since 2019.
Last reviewed by Arjen Karel on March 2026
Table of Contents!
- How Images and Media Cause Layout Shift (and How to Fix It)
- The browser does not know how big your image is
- How width and height prevent layout shift
- Why width:auto breaks your CLS
- When CSS aspect-ratio is the better choice
- Videos have the same problem
- Iframes default to 300x150 pixels
- Art direction: when mobile and desktop crops have different ratios
- Keep the same ratio across all srcset sources
- Do not lazy-load above the fold
- 1x1 pixel placeholders cause massive shifts
- Use object-fit for fixed-size containers
- Auto-playing carousels produce infinite CLS
- SVG, containment, and other edge cases
- How to find image CLS
- Quick fix checklist
- Where the web stands on image CLS
- Related guides
- Image and Media CLS Questions Answered
The browser does not know how big your image is
Every image layout shift comes down to one thing. The browser does not know how much space to reserve before the image loads.
When the browser encounters an <img> tag without dimensions, it renders the image at 0x0 pixels. The file arrives, the browser recalculates the layout, and everything below the image jumps down. That jump is your CLS.
How width and height prevent layout shift
In 2019 and 2020 all major browsers shipped a feature that changed how width and height attributes work. Browsers now use them to calculate an aspect ratio before the image downloads.
When you write this:
<img src="hero.jpg" width="800" height="450" alt="Description"> The browser internally generates this:
img[Attributes Style] {
aspect-ratio: auto 800 / 450;
} No image data needed. The browser knows the ratio and reserves the vertical space.
This internal ratio has the lowest CSS priority, so any CSS rule you write overrides it. That matters because it is the reason width: auto breaks everything (more on that in a moment).
The auto keyword tells the browser: use this ratio as a placeholder, but once the actual image loads, switch to the real dimensions. If you accidentally set wrong values (say width="4" height="3" on a 16:9 image), the browser reserves 4:3 space initially, then corrects to 16:9 when the image loads. That correction is a layout shift. Always use the actual pixel dimensions.
For responsive images, add this CSS:
<style>
img {
height: auto;
max-width: 100%;
}
</style> height: auto calculates the height from the ratio. max-width: 100% prevents the image from exceeding its container. The browser knows the width and the ratio. That is enough.
Why width:auto breaks your CLS
This is the one I see the most and the one that wastes the most debugging time. The developer sets width and height on every image, does everything right in the HTML, runs Lighthouse, gets a green score, ships it. Then CLS complaints come in from real users.
The cause is always somewhere in the CSS. One global rule undoes everything:
<style>
img {
width: auto;
height: auto;
max-width: 100%;
}
</style> The problem is width: auto. Because the browser's internal ratio has the lowest CSS priority, any rule overrides it. width: auto removes the width the browser was using to calculate the height. Both dimensions become unknown. The image renders at 0x0 until the file downloads, then snaps to full size.
Setting aspect-ratio in CSS does not fix this. With width: auto, the browser treats the width as 0 before the image loads. An aspect ratio calculated from 0 still produces 0. Zero times anything is zero.
Here is the part that makes this so hard to catch: it only causes a shift for first-time visitors. If the image is in the browser cache, the actual dimensions are available immediately and no shift occurs. You, as the developer, will never see it on your own machine. Your testers will not see it. Your QA team will not see it. Only your real users on their first visit see it, which is exactly the visit Google measures for CrUX.
I have spent hours on calls with engineering teams who swore their CLS was fixed because they could not reproduce it locally. It was always this. Check your CSS. If you have a global width: auto on images, that is your problem.
The fix:
<style>
img {
height: auto;
max-width: 100%;
}
</style> Remove width: auto. Keep height: auto and max-width: 100%. This is the pattern recommended by web.dev.
Quick check: right-click any image on your site, choose "Inspect Element", and look at the computed styles. If you see width: auto, now you know what to do. For the complete walkthrough, see fix layout shift caused by auto-sizing images.
When CSS aspect-ratio is the better choice
Width/height attributes are the default approach. But sometimes you want CSS to enforce a specific ratio regardless of the image content. A hero banner that must always be 16:9, or a product card grid where every image needs the same shape:
<style>
img {
aspect-ratio: 16 / 9;
width: 100%;
}
</style> Works in all modern browsers (Chrome 88+, Firefox 87+, Safari 15+) and on any element, not just images and videos.
For regular content images, width/height attributes are better. They describe the actual dimensions and self-correct when the image loads.
You can combine both:
<style>
img {
aspect-ratio: auto 16 / 9;
width: 100%;
}
</style> The auto keyword here means: use 16/9 before the image loads, switch to the natural ratio after. Space reservation before load, accurate sizing after.
Videos have the same problem
A video without width and height renders at 0x0 until the browser downloads enough of the file to read its metadata. On a slow connection, that takes seconds. The fix is identical to images:
<video src="demo.mp4" width="1280" height="720" autoplay muted loop></video> Browsers calculate an internal aspect-ratio: auto 1280 / 720 from the attributes. Add height: auto; max-width: 100%; in CSS and you are done.
One thing to watch: the poster image. If you set a poster but do not set dimensions on the video element, the element sizes itself to the poster. When the video starts playing and has a different resolution, you get a shift. Set width and height to match the video resolution, not the poster.
Iframes default to 300x150 pixels
An iframe without explicit dimensions defaults to 300x150 pixels. For most embeds (YouTube, Google Maps, social media posts) that is wrong. The moment the embed loads and resizes, everything around it shifts.
Unlike images, iframes do not automatically calculate an aspect ratio from their attributes. You have to set the sizing yourself:
<style>
.responsive-iframe {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}
</style> This replaces the old padding-top hack where you wrapped the iframe in a container with padding-top: 56.25% and positioned it absolutely. The old hack still works. aspect-ratio is cleaner and needs no wrapper.
Or do not load the iframe at all
For YouTube, Vimeo, Google Maps, and social embeds I stopped loading iframes on page load years ago. The facade pattern is better in every way: show a static placeholder image with the correct aspect ratio. When the user clicks, JavaScript swaps it with the real iframe. Zero iframe, zero CLS, zero wasted resources.
The shift from the swap happens within 500ms of user input and is excluded from CLS by design.
For implementation examples, see perfect YouTube embeds and Google Maps without losing PageSpeed.
Art direction: when mobile and desktop crops have different ratios
Art direction is when you serve different crops at different viewport widths. Wide landscape on desktop, tighter portrait on mobile. The <picture> element handles this with <source> elements.
The CLS problem: these crops usually have different aspect ratios. The browser needs to know which dimensions go with which breakpoint. You can set width and height on each <source>:
<picture>
<source
media="(max-width: 799px)"
srcset="hero-mobile.jpg"
width="480" height="600">
<source
media="(min-width: 800px)"
srcset="hero-desktop.jpg"
width="1200" height="400">
<img
src="hero-desktop.jpg"
width="1200" height="400"
alt="Product hero image">
</picture> Chrome and Safari pick up the correct width/height from whichever <source> is active. Firefox does not. There is a bug (bug 1694741) where Firefox always uses the <img> fallback dimensions, regardless of the selected source. If the mobile crop has a different ratio, Firefox shows a shift.
If all your crops share the same ratio (just different resolutions), this bug does not matter. It only fires when ratios differ between breakpoints.
Workaround for Firefox
Until the fix ships, use CSS media queries that match the breakpoints in your <source> elements:
<style>
picture img {
width: 100%;
height: auto;
}
@media (max-width: 799px) {
picture img {
aspect-ratio: 480 / 600;
}
}
@media (min-width: 800px) {
picture img {
aspect-ratio: 1200 / 400;
}
}
</style> This overrides the browser's internal ratio with explicit CSS per breakpoint. Works in all browsers.
Keep the same ratio across all srcset sources
Responsive images with srcset and sizes can cause CLS if the width/height attributes do not match the selected source. The rule is simple: all images in a srcset must share the same aspect ratio. If they do, one set of width/height on the <img> is enough:
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, 800px"
width="800" height="450"
alt="Hero image"> 800:450 is the same ratio across all three variants. No matter which one the browser picks, the reserved space is correct.
If you mix ratios in the same srcset (one source at 16:9, another at 4:3), the reserved space will be wrong for at least one of them. Do not do this. Use <picture> with <source> elements instead if you need different ratios.
When sizes is wrong
The sizes attribute tells the browser how wide the image will render at each viewport size. If it does not match the actual CSS layout, the browser picks a source that is too large or too small. This does not cause CLS (the ratio stays the same), but it wastes bandwidth or produces a blurry image.
sizes=auto for lazy-loaded images
Chrome 126+ and Edge 126+ support sizes="auto" on lazy-loaded images. The browser calculates the correct size from the CSS layout instead of relying on your manually written sizes attribute. Does not work on eager-loaded images because the browser needs the layout size before CSS has been parsed.
Do not lazy-load above the fold
Lazy loading images above the fold causes two problems at once. It delays the LCP because loading="lazy" hides the resource from the preload scanner. The browser cannot discover the image until after layout. And if the image also lacks dimensions, you get a layout shift on top of the delayed load.
The 2025 Web Almanac reports that 16% of pages still lazy-load their LCP image. That is 16% of the web delaying its most important image for zero benefit.
loading="lazy" belongs on images outside the initial viewport. Nowhere else.
1x1 pixel placeholders cause massive shifts
JavaScript lazy loading scripts often use a tiny 1x1 pixel transparent GIF as a placeholder in src while the real URL sits in data-src. When IntersectionObserver triggers, JavaScript swaps them.
This causes CLS because the 1x1 placeholder renders at 1px tall. When the 800x450 image loads, the element jumps from 1px to 450px. If you are still using a JavaScript lazy loading library in 2026, stop. Native loading="lazy" handles the placeholder internally and respects the width/height attributes. It has had full browser support since 2022.
If you have a specific reason to keep JavaScript lazy loading (intersection thresholds, fade-in animations), use a placeholder with the same aspect ratio as the final image. A 16x9 transparent SVG instead of 1x1.
LQIP done right
A blurred thumbnail placeholder looks better than empty space. The key: preserve the aspect ratio. If your final image is 800x450, your LQIP should be a tiny version (10x6 or 20x11) with the same ratio. Scale it up with CSS, apply a blur filter. When the real image loads, it replaces the blur at the same dimensions. No shift.
Next.js does this automatically. The Image component generates width, height, and blurDataURL at build time. Zero CLS.
Use object-fit for fixed-size containers
Product card grids where every card is the same height but the images have different ratios. I see this pattern constantly:
<style>
.product-image {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
object-position: center;
}
</style>
<img
src="product.jpg"
width="400" height="600"
class="product-image"
alt="Product name"> aspect-ratio: 1 / 1 forces a square. object-fit: cover fills the square and crops the overflow. The size is locked before the image loads.
This is also how you replace CSS background images with proper <img> elements. Background images cannot be lazy loaded, cannot use fetchpriority, and the preload scanner does not find them until the CSS file is parsed. An <img> with object-fit: cover gives you all the performance controls a background image lacks.
Auto-playing carousels produce infinite CLS
Slide transitions that animate left, width, or margin trigger layout recalculation. Because autoplay is not user input, every shift counts toward CLS. An auto-playing carousel with layout-triggering transitions can produce CLS indefinitely.
Fix the container dimensions with a fixed aspect ratio, animate with transform: translateX() instead of left or margin-left (transforms run on the GPU and never trigger layout), and if you must autoplay, the slide container dimensions cannot change. Any resize counts as CLS.
In a non-autoplay carousel, most transitions happen within 500ms of a user click and are excluded from CLS. Autoplay removes that protection. The web.dev carousel guide covers the implementation details.
SVG, containment, and other edge cases
SVGs loaded via <img> need width and height on the tag, same as raster images. Inline <svg> elements need explicit dimensions or a viewBox with CSS aspect-ratio. An SVG with neither defaults to 300x150.
For embeds you cannot control (ad slots, third-party widgets, user-generated content), use CSS containment to stop the shift from spreading:
<style>
.embed-container {
min-height: 250px;
contain: layout style;
}
</style> contain: layout style tells the browser that nothing inside this container affects the layout outside it. The embed can shift, grow, or shrink. The rest of the page stays put.
For below-the-fold sections with heavy media, content-visibility: auto with contain-intrinsic-size skips layout for off-screen content. Without contain-intrinsic-size, the element has zero height and the expansion when the user scrolls to it is a layout shift:
<style>
.below-fold-gallery {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
</style> The auto keyword means: start with 500px as an estimate, but once the browser renders it and knows the real height, remember that value.
How to find image CLS
You will not catch image CLS on your own machine. Your browser cache already has the dimensions. You need first-time visitor conditions or real user data.
Real User Monitoring
Start with CoreDash or another RUM tool. CLS attribution data shows exactly which elements shifted. Just navigate to CLS and look at the attribution Element table. It will tell you exactly which elements shifted. To find images just filter for image and you will get a table of all the image elements affected by layout shifts ordered by impact.

Chrome DevTools
Disable the cache in the Network tab, throttle to Slow 4G, enable screenshots, reload. Watch for visual jumps. Then open the Performance panel and look for "Layout Shift" entries. Click a shift to see the node, the score, and whether it had recent user input.
Core Web Vitals Visualizer
The Core Web Vitals Visualizer extension highlights every layout shift with a colored overlay. I use this as my first step before opening the Performance panel. Reload with the extension active and you see exactly what moved.
Lighthouse catches part of it
Lighthouse flags images without width and height. But it misses the width: auto problem entirely because the HTML attributes are present. CSS is overriding them, and Lighthouse does not check computed styles. This is why I never trust a green Lighthouse CLS score without also checking field data.
Quick fix checklist
| Element | CLS cause | Fix |
|---|---|---|
<img> | Missing width/height | Add width and height; use height: auto; max-width: 100%; in CSS |
<img> | CSS width: auto overriding dimensions | Remove width: auto; keep only height: auto |
<video> | Missing width/height | Add width and height matching the video resolution |
<iframe> | Default 300x150 | CSS aspect-ratio: 16 / 9; width: 100%; or the facade pattern |
<picture> | Different ratios per source (Firefox bug) | Set width/height on each <source>; add CSS media query fallback |
<img srcset> | Mixed ratios in srcset | Same ratio for all sources; set width/height on the <img> |
| Lazy loaded images | Above-the-fold with no dimensions | Remove loading="lazy"; always set dimensions |
| JS lazy loading | 1x1 placeholder with wrong ratio | Use native loading="lazy" or match placeholder ratio |
| Carousel | Autoplay + layout-triggering transitions | Fixed aspect-ratio container; transform for transitions |
| SVG | No built-in dimensions | Width/height on <img> or viewBox + CSS aspect-ratio |
| Ad slots / embeds | Unknown dimensions | min-height + contain: layout style |
Where the web stands on image CLS
The 2025 Web Almanac numbers:
- 62% of mobile pages have at least one unsized image. Down from 66% in 2024. Still the majority.
- 65% of desktop pages have unsized images. Down from 69%.
- At the p75, a desktop page has 9 unsized images. Mobile has 8.
- At the p90: 25 on desktop, 22 on mobile.
- Median unsized image height: 111px on desktop, 98px on mobile. Enough to shift a paragraph.
- 72% of desktop and 81% of mobile origins pass CLS. Up from 62% in 2021.
- 40% of mobile pages still use non-composited animations that cause CLS on their own.
CLS has improved more than any other Core Web Vital over the past four years. Unsized images are still the biggest contributor. Fix this one issue and layout shifts disappear on most sites.
Related guides
- What is Cumulative Layout Shift (CLS): The complete guide. Formula, thresholds, session windows, and all CLS causes beyond images.
- Find and Fix CLS Issues: Step-by-step diagnostics with RUM data, DevTools, and fixes for every cause.
- Fix layout shift caused by auto-sizing images: The full
width: autowalkthrough. - Optimize images for Core Web Vitals: Preloading, responsive images, formats, prioritization.
- Layout shift caused by CSS transitions: How layout-triggering animations cause CLS.
- Perfect YouTube embeds: The facade pattern for zero CLS.
- Google Maps 100% PageSpeed: Same facade approach for Maps.
Find out what is actually slow.
I map your critical rendering path using real field data. You get a prioritized fix list, not a Lighthouse report.
Get the auditImage and Media CLS Questions Answered
Why does width:auto on images cause layout shift even when width and height attributes are set?
The browser's internal aspect ratio (calculated from width/height attributes) has the lowest CSS priority. width: auto overrides it, making both dimensions unknown. The image renders at 0x0 until the file downloads. Remove width: auto and keep only height: auto; max-width: 100%;.
Do I need width and height on video and iframe elements too?
Yes for video. The same width/height mechanism works. Set the attributes to match the video resolution, not the poster image. Iframes are different: they do not calculate a ratio from attributes and default to 300x150. Use CSS aspect-ratio or the facade pattern.
How do I prevent CLS when using the picture element with different aspect ratios per breakpoint?
Set width and height on each <source>. Chrome and Safari use the correct dimensions from the active source. Firefox has a bug (1694741) where it always uses the <img> fallback. Add CSS media queries with the correct aspect-ratio per breakpoint as a workaround.
Does lazy loading cause layout shift?
Not if the image has width and height attributes. But lazy loading above-the-fold images delays LCP for no benefit. Never use loading="lazy" on images in the initial viewport.
Why does Lighthouse show good CLS but my field data shows layout shifts?
Lighthouse runs on a warm browser with fast network. It does not catch the width: auto problem because it checks HTML attributes, not computed CSS styles. It also does not test post-load shifts from ads, embeds, or lazy-loaded content. Always verify CLS with field data from CrUX or a RUM tool like CoreDash.

