To fix CLS (Cumulative Layout Shift), find the element that moves after the page starts rendering, then give it a fixed space before it loads. In practice that means width and height on every image and iframe, reserved slots for ads and embeds, no banners injected above existing content, web fonts that swap without changing size, and animations built with transform.
A good CLS score is 0.1 or less for 75% of page visits. CLS is usually the cheapest Core Web Vital to repair, because most fixes are a few lines of HTML or CSS.
I’ve fixed layout shift on a lot of WordPress and Shopify sites, and the pattern rarely changes. The hard part of how to fix CLS isn’t the fix. It’s catching the exact element that jumps, so that’s where this guide starts.
What Counts as a Layout Shift?
A layout shift happens when a visible element changes its start position between two frames. Web.dev’s CLS documentation scores each shift as impact fraction times distance fraction: how much of the viewport the moving content touches, multiplied by how far it moved.
CLS doesn’t simply add up every shift over the visit. Shifts are grouped into session windows, where each shift happens within 1 second of the previous one and a window lasts 5 seconds at most. Your CLS is the window with the highest total.
Thresholds are 0.1 or less for good, above 0.1 up to 0.25 for needs improvement, and above 0.25 for poor. Measured at the 75th percentile, like the other Core Web Vitals.
Two things don’t count. Shifts within 500 ms of a click, tap or key press are treated as expected, since the user asked for a change. And animations using transform don’t trigger layout shifts at all. For how CLS relates to LCP, INP and your PageSpeed score, see my breakdown of page speed vs Core Web Vitals.
How to Fix CLS Without Guessing: Catch the Shift First
Don’t guess. I’ve watched people add image dimensions everywhere and still fail, because the real culprit was a font or a cookie notice.
Start with field data in PageSpeed Insights or the Search Console Core Web Vitals report. That tells you whether real users see shifts and which group of pages fails. Then reproduce it in Chrome DevTools with these three tools.
- Layout Shift Regions. Open the Rendering tab, tick Layout Shift Regions and reload. Areas that shift flash purple, so you can literally watch the jump.
- Performance panel live metrics. The panel shows your local CLS as you load and scroll, and its Layout shifts tab lists each shift with the elements involved and the score.
- A recorded trace. Record a reload in the Performance panel. The Layout Shifts track shows clusters of shifts, and clicking one reveals the elements that moved.
Scroll the whole page slowly while you test. Many shifts happen below the fold, when lazy-loaded images or ad slots arrive as you reach them.
Test on a phone-sized viewport too. Narrow screens wrap text into more lines, so a headline that sits still on a wide monitor can bounce around on mobile. I also throttle the network, since a slow connection stretches the gap between first paint and the moment late assets land. That gap is exactly where shifts breed.
Log the Culprit From Real Visitors
If you can’t reproduce the shift, let real users tell you. The web-vitals library’s attribution build returns the largest shift and the element that moved, per its GitHub README:
import {onCLS} from 'web-vitals/attribution';
onCLS(({value, attribution}) => {
const {largestShiftTarget, largestShiftValue, largestShiftTime, loadState} = attribution;
// Replace console.log with your analytics call.
console.log('CLS', value, {largestShiftTarget, largestShiftValue, largestShiftTime, loadState});
});
largestShiftTarget is a CSS selector for the element that shifted. After a few days of data you’ll have a ranked list of offenders instead of a hunch.
Which Element Moved? Match It to Its Cause
Once you know what moved, the cause is usually obvious. This table is the lookup I use.
| What you see | Likely cause | Fix section below |
|---|---|---|
| Text jumps down as an image appears | Image or iframe without dimensions | Images and iframes |
| Content drops when an ad or video fills in | Embed with no reserved space | Ads and embeds |
| The whole page slides down after load | Banner, notice or promo bar injected at the top | Late-injected content |
| Text reflows or changes width a moment after load | Web font swapping with different metrics | Web fonts |
| Nearby elements jiggle during an effect | Animation of top, left, width or height | Animations |
Fix Images and Iframes Without Dimensions
This is the most common CLS cause I find. Without size information, the browser gives an image zero height, then pushes everything down when the file arrives.
Add width and height attributes that match the image’s real proportions. Modern browsers use them to compute an aspect ratio before the image loads, and your CSS can still make the image responsive:
<img src="team-photo.jpg" width="1200" height="800" alt="Blue ceramic coffee mug on a wooden desk">
img {
max-width: 100%;
height: auto;
}
The height: auto part matters. It lets the image scale with the layout while keeping the reserved ratio.
Iframes and video embeds need the same care. When you can’t set fixed dimensions, reserve the box with the aspect-ratio property, which MDN’s compatibility data lists in Chrome 88+, Firefox 89+ and Safari 15+:
.video-embed {
width: 100%;
aspect-ratio: 16 / 9;
}
How Do You Stop Ads and Embeds From Pushing Content?
Ads are tricky because you often don’t know the final size. The fix is to reserve space anyway, using the most likely size as a min-height:
.ad-slot-top {
min-height: 250px;
}
Web.dev also suggests placing late-loading slots lower on the page where possible, and not collapsing the reserved space when no ad fills it. An empty gap looks slightly odd. A jump is worse.
Social embeds, review widgets and map embeds follow the same rule. Wrap each one in a container with a fixed minimum height before the third-party script runs.
Why Does Late-Injected Content Shift the Page?
Cookie notices, promo bars, “subscribe” strips and app install banners often get inserted at the top of the page after the first render. Everything below them slides down, and that one move can blow through the 0.1 threshold by itself.
You have three good options. Reserve the space in the initial HTML so the banner fills a box that already exists. Show it as an overlay with position: fixed so it doesn’t push content. Or place it at the bottom of the viewport, where it moves less of the page.
The same goes for content loaded after an API call, like “related products” appearing above the fold. Web.dev’s advice is to load new content in response to a user action, like a “Load more” button, since shifts within 500 ms of an interaction aren’t counted.
Why Do Web Fonts Cause Layout Shift?
When a web font loads late, the browser first shows fallback text or invisible text. Then it swaps in the real font. If the two fonts have different widths or line heights, lines rewrap and the paragraphs below move.
You have a few levers here, all from web.dev’s Optimize CLS guide:
font-display: optional. The browser uses the web font only if it’s ready almost immediately, so a late font never causes a re-layout.- Preload critical fonts with
<link rel="preload">so they arrive before first paint. - Match the fallback font’s size with
size-adjust, plusascent-overrideanddescent-override, so the swap barely changes anything.
Here’s the shape of a matched fallback. The percentage below is an example only, so measure your own font pair before using it:
@font-face {
font-family: "Brand Fallback";
src: local("Arial");
size-adjust: 105%;
}
body {
font-family: "Brand Font", "Brand Fallback", sans-serif;
}
MDN lists size-adjust in Chrome 92+, Firefox 92+ and Safari 17+. In my experience this is the fix that surprises clients most, because font shifts are small and easy to miss by eye.
Stop Animating Layout Properties
Animating top, left, width, height or margin changes layout, and it can nudge other elements. Use transform: translate() or transform: scale() instead. Web.dev confirms transform-based animations don’t count toward CLS.
/* Shifts layout */
.drawer { transition: top 0.3s; }
/* Does not shift layout */
.drawer { transition: transform 0.3s; }
Sliders and carousels are the usual offenders here, especially older ones that animate left. Accordions and expanding FAQ panels are fine, by the way. They move content right after a click, which falls inside the 500 ms grace period, as long as the panel opens promptly.
Does the Back/Forward Cache Help CLS?
Yes, indirectly. Web.dev’s CLS docs say a page restored from the back/forward cache (bfcache) should have CLS reset to zero, since users see it as a new visit. A page restored instantly doesn’t reload images and ads, so it doesn’t shift again.
To check eligibility, open DevTools, go to Application, then Back/forward cache, and click Run test. The most common blocker web.dev names is an unload event listener, which some older plugins and scripts still add. Removing it lets more of your back-button visits restore instantly.
Where CLS Hides on WordPress and Shopify
On WordPress, I look at sliders, popup plugins, lazy-load plugins that strip image dimensions, and page builder sections that load their CSS late. Heavy builders are a recurring theme, and I compare them in Elementor vs block editor SEO.
On Shopify, the usual suspects are apps that inject widgets after load: review stars, announcement bars, upsell blocks and currency switchers. Check whether each app gives the widget a fixed container. If it doesn’t, reserve one in your theme, then work through the rest of the store layer, from app bloat to the LCP image, with my guide to speeding up a Shopify store.
My quickest test on either platform? Disable one plugin or app at a time on a staging copy and reload with Layout Shift Regions switched on. When the purple flashes stop, you’ve found the troublemaker. Boring, yes. Reliable, too.
When the Fix Needs a Developer
Most CLS work is HTML and CSS you can handle yourself. It gets harder when the shift comes from a JavaScript framework re-rendering the page, a theme you can’t edit, or an ad network that ignores your containers. That’s the point where a performance trace and code access matter more than another plugin.
If you want that handled, it’s part of our technical SEO service. If slow taps are your other problem, my guide on how to improve INP covers it, and LCP will get its own guide. For a quick diagnosis first, request a free SEO audit.
Frequently Asked Questions
What Is a Good CLS Score?
A good CLS score is 0.1 or less at the 75th percentile of page loads. Scores above 0.1 up to 0.25 need improvement, and anything above 0.25 is poor, according to web.dev.
Why Is My CLS Bad in the Field but Fine in Lighthouse?
A lab test loads the page once without scrolling or clicking. Real visitors scroll, trigger lazy-loaded images and see ads or banners that the lab run may miss, so field CLS is often higher.
Does Lazy Loading Cause CLS?
Lazy loading itself doesn’t. Lazy-loaded images without width and height do, because the browser can’t reserve space for them. Keep the dimensions and the shift disappears.
How Long Until a CLS Fix Shows in Search Console?
Search Console and PageSpeed Insights use a rolling 28-day window of Chrome user data. Expect the number to improve gradually and reach its new level after about four weeks.
Last updated: September 2026 by Mizanur Rahman



