Images Causing Layout Shift
An image without a known size starts life as a box with no height. The text beneath it is drawn straight away, then the file arrives, the box grows to the picture's real height, and everything below is shoved down the screen. Readers lose their place and tap the wrong link, and the page's Cumulative Layout Shift score rises.
This guide shows how to confirm an image is the cause, give every image a size the browser can use before downloading it, handle the awkward cases such as CSS backgrounds, SVGs and lazy loading, and keep new uploads from bringing the problem back.
Why an unsized image moves the page
A browser lays out a page before most of its files have arrived. When it reaches an <img>
tag, it needs a width and a height to know how much room to leave. If the tag says nothing about size
and no CSS rule fixes the height, the browser has to guess, and its guess is zero pixels tall. The paragraph
after the image is placed directly under the heading above it.
A moment later the image data downloads. The browser now reads the picture's real pixel size from the file header, works out how tall it should be at the current width, and re-lays out the page. The paragraph that was sitting just under the heading is now several hundred pixels lower. That jump is a layout shift, and the CLS Debugger will report the paragraph, and everything after it, as the elements that moved.
How the attributes fix it
Modern browsers use the width and height attributes on an image in a clever
way. Their default stylesheet turns the two numbers into an aspect ratio, roughly equivalent to aspect-ratio: auto 1200 / 800.
So even when your CSS makes the image fluid with width: 100%, the browser can calculate
the right height for whatever width the column happens to be, before a single byte of the file has arrived.
The box is reserved, the text is placed underneath it, and nothing moves when the picture fills in.
This is why the attributes do not have to match the displayed size. They describe the shape
of the image. A 1600 by 900 photo shown at 400 pixels wide is fine with width="1600" height="900",
as long as your CSS lets the height follow the width.
Proving it is an image and not something else
A paragraph moving down does not always mean an image above it was unsized. Fonts, ad slots and banners produce the same symptom. Before editing templates, make sure the mover and the cause line up.
- Run the CLS Debugger on the page and note the largest shifts and the elements it names. Look at what sits directly above each named element in the page.
- In Chrome DevTools, open the Network panel, set throttling to a slow profile and tick Disable cache. Reload and watch the page. An image problem looks distinctive: text appears, then a picture pops in and the text drops by roughly the height of the picture.
- Open the Elements panel, select the image and look at its tag. If there are no
widthandheightattributes, check the Computed tab foraspect-ratioandheight. If the computed aspect ratio isautowith no numbers and the height isauto, the browser had nothing to reserve. - As a quick proof, block the image in the Network panel (right-click the request and choose block request URL) and reload. If the page no longer jumps at that point, the image was the cause.
You can also list every image on a loaded page that is missing size information by pasting this into the console:
[...document.images].forEach((img) => {
const cs = getComputedStyle(img);
const sized = img.getAttribute('width') && img.getAttribute('height');
if (!sized && cs.aspectRatio === 'auto') {
console.log(img.currentSrc || img.src, img);
}
});
Anything it prints is an image the browser could not size before download, unless a fixed CSS height is doing the job instead.
The fix, step by step
Step 1
Confirm the shift comes from an image
Run the CLS Debugger and write down the top shifts, the elements named and roughly when they happen. Reproduce the load in Chrome with slow network throttling and the cache disabled, and watch for text dropping at the moment a picture appears. Block that image request and reload: if the jump disappears, you have found the cause. If it does not, look at fonts, ads or inserted notices instead of editing images.
Step 2
List every unsized image on the template
Paste the console snippet from this guide into DevTools on each main template, such as the home page, a blog post, a product page and a category page. Note which images come from the theme, which come from editors writing content, and which are added by plugins or apps. Fixing the source that generates the tag, rather than individual pages, is what stops the problem from coming back next week.
Step 3
Add the intrinsic width and height
Give
each <img> tag width and height attributes equal to the
real pixel size of the file, without units, for example width="1200" height="800". In templates,
read the numbers from the media library or image metadata rather than typing them. For srcset
images, use the dimensions of any candidate with the same proportions. For inline SVG icons, set the
attributes or a viewBox plus a CSS size.
Step 4
Let CSS scale the image without losing the ratio
Add a rule such as img { max-width: 100%; height: auto; } so the
width follows the column and the height is calculated from the attributes. Search the stylesheet for
rules that set a fixed height on images without also fixing the width, because they stretch the picture.
Where a design needs a crop, set both dimensions or an aspect-ratio and add object-fit: cover
so the image fills the box without distortion.
Step 5
Reserve space for images you cannot tag
Some pictures have no tag you control: CSS backgrounds on hero sections, images injected by
a gallery script, or pictures whose size is unknown until an API responds. Give their wrapper an explicit
aspect-ratio, such as aspect-ratio: 16 / 9, or a fixed min-height
matching the finished layout. The box then keeps its size while empty, and the image fills it without
pushing anything else down the page.
Step 6
Keep lazy-loaded images in sized boxes
Lazy loading only delays the download; it does not reserve space, so every lazy image still
needs its attributes or a sized wrapper. Replace older JavaScript lazy loaders that swap a one-pixel
placeholder for the real file with native loading="lazy". Do not lazy-load the main image
near the top of the page, because it delays your largest content and gives the box less time to be filled
before the visitor starts reading.
Step 7
Re-test slowly and on a narrow screen
Clear any page cache and CDN cache, then re-run the CLS Debugger on each template you changed. Reproduce the load again with throttling at a phone width and at a desktop width, scrolling all the way down so lazy images below the fold load as well. Check that no picture looks stretched or squashed. Then watch real-user CLS over the following weeks as older visits drop out of the report.
Before and after markup
Here is the pattern that causes the problem, followed by the corrected version. The business, file names and sizes are fictional.
<!-- Before: no size information, so the box starts at zero height -->
<img src="/media/greenbank-bakery-shopfront.jpg" alt="Greenbank Bakery shopfront">
<!-- After: intrinsic size in attributes, fluid in CSS -->
<img src="/media/greenbank-bakery-shopfront.jpg"
width="1600" height="1067"
alt="Greenbank Bakery shopfront">
/* In the stylesheet */
img {
max-width: 100%;
height: auto;
}
/* A cropped card image that must always be square */
.card img {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
}
Background images in CSS
A hero section that uses background-image has no intrinsic size at all, because the element's
height comes from its content, not from the picture. That usually causes no shift on its own. The problem
appears when a script measures the image and then sets the section's height, or when the section is empty
until the image loads. Set the size up front:
.hero {
background: #e8e2d8 url("/media/hero-kitchen.jpg") center / cover no-repeat;
aspect-ratio: 21 / 9;
min-height: 280px;
}
The background colour gives a calm placeholder while the file downloads, and the box never changes size.
Images inside flex and grid layouts
Inside a flex row, an image with height: auto and attributes behaves well, but a rule
like align-items: stretch on the parent, or min-width: 0 missing on a flex
child, can make the reserved ratio be ignored until load. If a card grid still jumps after adding attributes,
inspect the card and check whether the image wrapper has its own height set by the flex or grid algorithm.
Giving the wrapper an explicit aspect-ratio is the most reliable fix.
Awkward cases and how to handle them
| Situation | Why it still shifts | What to do |
|---|---|---|
| Editor-uploaded images in articles | The rich text editor or a Markdown converter writes
a bare tag with only src and alt. | Add dimensions when the content is rendered, reading them from the stored file. Most CMSs can do this in an output filter. |
| SVG logos and icons | An SVG with no width, height or viewBox
has no ratio the browser can use. | Add a viewBox inside the file and size the <img>
or inline SVG in HTML or CSS. |
| Images from a third-party feed | Review widgets, Instagram grids and product feeds insert pictures of unknown size. | Fix the size of the container, crop with object-fit, and
request thumbnails of a fixed size where the feed allows it. |
| Wrong numbers in the attributes | A theme hard-codes 800 by 600 while the uploaded file is 1200 by 1600. | Generate the attributes from the real file. A fixed crop on the server also guarantees the shape. |
| Placeholder swapped for the real image | An old lazy loader shows a 1 by 1 GIF, then swaps in the real picture with a different ratio. | Use native lazy loading, or keep attributes on the tag so the ratio is right from the start. |
| Image hidden until loaded | A fade-in script sets display: none until the
load event. | Use opacity for the fade so the box keeps its space. |
img { height: 300px; }. It removes the jump but squashes or stretches pictures at every
other screen width. Let the height follow the width and the ratio.If a picture swaps between files with different proportions at different screen sizes, for example a wide banner on desktop and a tall one on phones, that is a different problem with a different fix, covered in fixing CLS from responsive image swap.
Where the tags come from on common platforms
The fix is the same everywhere, but where you apply it depends on how your site generates image tags.
- WordPress. Core adds dimensions to images inserted through the block editor from the media library, but page builders, older themes, custom fields and shortcodes often print bare tags. Check the template or widget that outputs the tag rather than the post content. Plugin-specific patterns are covered in CLS in WordPress.
- Shopify. Liquid's
image_tagfilter outputs width and height from the image object, while hand-written<img src="{{ image | image_url }}">tags in older themes do not. See CLS in Shopify for theme and app specifics. - React and Next.js. The Next.js image component requires a width and height, or a
fillmode inside a sized parent, which is designed to prevent this problem. Plain<img>tags in components need attributes like any other HTML. - Static sites and hand-written HTML. Build tools and static site generators can read file dimensions at build time and write them into the tag. If yours cannot, add them by hand for the images above the fold first.
alt text and file size
too. Adding dimensions does not make a large image load faster; it only stops it moving things around.Keeping new images sized
Image shifts tend to come back quietly, because new content is added every week by people who are not thinking about layout. A few habits keep the problem closed:
- Make the template responsible. The code that renders an image should always add width and height from stored metadata. Editors should never have to type them.
- Add a check to your build or review process. A simple test that fails when an
<img>tag in the output lacks both attributes catches regressions before they go live. - Standardise crops. Where a layout has fixed shapes, such as square product cards or 16:9 article thumbnails, crop images to that shape on upload or in the image service so the ratio is always predictable.
- Re-run the debugger after redesigns. New themes and new components are the most common time for bare image tags to reappear.
For how image dimensions fit with compression, formats and lazy loading more generally, see the image optimisation fixes.
1. Do width and height attributes make my images fixed size and stop them being responsive?
No, as long as your CSS sets the height to auto. The attributes tell the browser the proportions of the image before it downloads. A rule such as max-width 100 per cent with height auto then lets the image shrink or grow with the column while keeping that shape. The numbers are only used as a starting ratio and as a fallback size if no CSS applies, so a 1600 by 900 image can still display at 350 pixels wide on a phone.
2. What numbers should I put in the width and height attributes?
Use the actual pixel dimensions of the image file, or any pair of numbers with exactly the same proportions. If the file is 2400 by 1600, then 2400 and 1600 or 1200 and 800 both give the same ratio. What matters is the shape. Numbers that do not match the file make the browser reserve the wrong box, which then corrects itself when the image arrives, so the page still shifts, only by a smaller amount than before.
3. Is the CSS aspect-ratio property better than width and height attributes?
They do the same job for layout, and you can use either. Attributes are best for ordinary content images because they travel with the tag and describe the file itself. The aspect-ratio property is better when the shape belongs to the design, such as every product card being square, or when there is no img tag at all, such as a CSS background or an empty container that a script fills later. Many sites use attributes on images and aspect-ratio on wrappers.
4. Does native lazy loading cause layout shift?
Lazy loading does not cause shift by itself, but it makes an existing sizing problem more visible. Because a lazy image loads as the visitor scrolls towards it, an unsized one expands while they are reading nearby text, and that movement counts towards CLS because scrolling does not exclude shifts. If every lazy image has width and height attributes or sits in a sized box, it can load at any time without moving anything.
5. Why does my page still shift after I added width and height to every image?
The most common reasons are a CSS rule that overrides the height, such as a fixed height or height set by a script, attributes with the wrong numbers, a lazy loader that swaps in a placeholder of a different shape, or a cause that is not an image at all. Re-run the CLS Debugger and check which elements now move and when. Web fonts, cookie notices and ads often produce a very similar downward jump.
6. Do small icons and logos need width and height too?
Yes. A small icon at the top of the page can still shift the header or a line of text when it loads, and a logo in the site header shifts everything below it on every page. SVG files are a frequent culprit because many are exported without a viewBox or size. Give each icon and logo explicit dimensions in HTML or CSS, and check the header in particular, since it affects every page.
7. Will fixing image shifts improve my rankings?
It may help a little, but it is not a large ranking factor on its own. Core Web Vitals are part of Google's page experience signals, and a page with good CLS meets one of the three thresholds. Content relevance matters far more. The clearer benefit is to visitors: text that stays put is easier to read, and buttons that do not move get fewer mistaken taps, which tends to help enquiries and sales.
The CLS Debugger records every layout shift while your page loads, names the elements involved, calculates the cumulative score and ranks each shift by its impact, so you can see which images to size first.
Open CLS Debugger Fixes โ