Fixing Layout Shift (CLS): The Overview
Cumulative Layout Shift is the Core Web Vital for content that jumps while someone is reading or about to tap it. A score of 0.1 or less at the 75th percentile of real visits counts as good.
Most CLS failures come from a handful of template habits: media without reserved space, fonts that swap late, ads and banners that push content down, and widgets that appear after the page has drawn. This overview shows how to confirm CLS is really your failing metric, capture the element that moves for real visitors, and send you to the guide for that exact cause.
Why CLS fails and where to look first
A layout shift happens when something that is already visible changes position because something else changed size or appeared above it. The CLS score for a visit is the total of the worst burst of shifts, and the score Google uses is the 75th percentile of those visit scores across real Chrome users over 28 days, for mobile and desktop separately.
| CLS at the 75th percentile | Rating |
|---|---|
| 0.1 or less | Good |
| Over 0.1 up to 0.25 | Needs improvement |
| Over 0.25 | Poor |
CLS is often the quickest of the three Core Web Vitals to fix, because the cause is normally one element on a shared template. Once you know which element moves, the fix is usually a few lines of HTML or CSS. The hard part is the finding, and the most common mistake is trusting a lab test that never saw the shift at all.
The eight causes behind most failures
| What you see | Likely cause | Detailed guide |
|---|---|---|
| Text jumps down when a photo appears | Image with no width and height or aspect ratio | Images without dimensions |
| Paragraphs reflow a moment after the page appears | Web font swapping in with different metrics | Web font swap |
| Content drops when an advert fills in | Ad slot with no reserved height | Ad slots |
| A review widget, carousel or recommendations block pops in | Component rendered late by JavaScript | Late-loaded components |
| A notice or "related items" row is inserted above what you are reading | Content injected above existing content | Dynamic content insertion |
| A hero changes height when a different image size loads | Responsive images with different shapes per breakpoint | Responsive image swap |
| A YouTube, map or social embed grows from nothing | Iframe with no reserved box | Video and iframe embeds |
| The whole page slides down under a strip at the top | Cookie, promo or cart banner pushing content | Cart and notification banners |
This page stays at the level of finding and triage. Each guide in the right-hand column goes into its cause in detail, with code for that case.
Why your CLS numbers disagree
CLS causes more confusion between tools than any other metric, because a lab test and a real visit are measuring different things.
- A lab test only watches the load. Lighthouse loads the page once, does not scroll and does not click. Shifts caused by images lazy-loading further down, a sticky header changing height on scroll, or a pop-up after ten seconds never happen in the test.
- Real visits last longer. The browser keeps measuring for the whole time the page is open. A long article with an advert that loads halfway down can fail in the field while scoring zero in the lab.
- Real visitors see the consent banner. Depending on how your test is set up, a lab run may or may not show the same cookie banner and personalised content a first-time visitor gets.
- Screens differ. A shift that affects a large part of a narrow phone screen scores higher than the same shift on a wide desktop, because the score depends on the share of the viewport that moved.
So the rule is simple: use field data from the Core Web Vitals checker or Search Console to decide whether you have a CLS problem, and use real-user attribution to find which element is responsible. Only then use lab tools to reproduce and test the fix.
What is not counted
Movement within half a second of a tap, click or key press is excluded because the visitor caused
it, so an accordion opening when clicked does not count. Animations using CSS transform
do not count either. Shifts during scrolling are still counted. The CLS
Debugger fixes page explains exactly how each shift is scored and grouped.
Capturing the element that shifts for real visitors
The web-vitals library's attribution build reports, for the worst burst in each visit,
the element that moved most and when it happened. A few days of this data tells you more than any number
of lab runs.
import { onCLS } from 'web-vitals/attribution';
onCLS(({ value, rating, attribution }) => {
sendToAnalytics({
metric: 'CLS',
value: Number(value.toFixed(3)),
rating,
target: attribution.largestShiftTarget, // CSS selector of the element that moved most
shiftValue: attribution.largestShiftValue,
shiftTime: Math.round(attribution.largestShiftTime),
loadState: attribution.loadState,
width: window.innerWidth
});
});
Group the results by target and width. A selector such as main > article > p
tells you that ordinary text moved, so the cause is something above it growing. The time and load state
narrow it further: a shift at 300 milliseconds while the page is still loading points to fonts or images;
one at several seconds after load complete points to adverts, embeds or banners.
Watching shifts directly in the browser
For a quick look on a single page, paste this into the DevTools console, then reload and use the page as a visitor would. Each shift is logged with the elements that moved.
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue;
console.log(entry.value.toFixed(4),
entry.sources.map(s => s.node));
}
}).observe({ type: 'layout-shift', buffered: true });
Remember the element listed is the one that moved, not the one that caused it. If a paragraph moves, look at what sits directly above it and grew.
The fix, step by step
Step 1
Confirm CLS is failing in field data
Run your key pages through the Core Web Vitals checker and read the field CLS for mobile and desktop, then open the Core Web Vitals report in Search Console to see which URL groups are poor or need improvement. Note whether the problem is one template, such as product or article pages, or the whole site. If only the lab result is high and field data is good, CLS is not your priority; move to the metric that is actually failing.
Step 2
Capture the shifting element from real visits
Add the web-vitals attribution build to the affected template and send
the largest shift target, its value, its time, the load state and the viewport width to your analytics.
After a few days, group the reports by target. The two or three selectors that account for most poor
visits are what you need to fix. Record whether each shift happens during loading or well after, since
that points to different causes.
Step 3
Replay the shift with layout shift regions
Open the page in Chrome with DevTools, open the Rendering tab from the More tools menu and tick Layout Shift Regions, which flashes a blue highlight over anything that moves. Throttle the network to a slow connection so late content arrives visibly late, clear the cache, and scroll the page as a visitor would. For exact timings, record in the Performance panel and select the entries in the Layout shifts track.
Step 4
Match the shift to its cause
Look directly above the element that moved and find what grew or appeared. Use the cause table on this page: an image or iframe with no reserved box, text reflowing when a font arrives, an advert or banner filling in, or a widget inserted by JavaScript. Check the page source for the element that grew; if it is not there, JavaScript added it later. Open the matching guide for the detailed fix for that cause.
Step 5
Reserve the space in the template
Make
the change in the shared template, not in one page, so every URL in the Search Console group benefits.
Most fixes come down to giving the late element a box of the right size before it arrives: width
and height attributes on images, aspect-ratio on embeds, a min-height
on ad and widget containers, overlay positioning for banners, and matched fallback font metrics. Avoid
inserting new content above anything the visitor can already see.
Step 6
Check the fix across page types and screen sizes
Repeat the layout shift regions test on a narrow phone width, a tablet width and a desktop width, since reserved heights that suit one width can be wrong at another. Test logged out with a cold cache and the consent banner showing. Deploy, then watch your attribution data for the old selector disappearing. Field CLS in the Chrome UX Report and Search Console improves over 28 days as old visits age out.
Where CLS usually comes from on each platform
WordPress
Older themes and some page builders output images without dimensions, and sliders often start at zero height and grow once their script runs. Cookie notices and "announcement bar" plugins frequently push the whole page down. Check theme and plugin output before writing custom CSS. The CLS in WordPress guide covers theme and plugin fixes.
Shopify
Review apps, "frequently bought together" blocks and currency or shipping bars injected by apps are the usual suspects, alongside product galleries whose images have different shapes. See CLS in Shopify.
React, Next.js and single-page apps
Components that render a spinner of one height and then content of another, data fetched on the client after the first paint, and hydration differences between server and client HTML all produce shifts. Route changes in a single-page app can also add shifts to a long-lived session. See CLS in React and Next.js.
Hand-written and static sites
These are usually the easiest to fix because you control every tag. The common gaps are missing image attributes in older content, third-party embeds pasted in as raw iframes, and a web font loaded from an external service without a matched fallback.
Edge cases that catch people out
- Shifts you cannot see. A shift of one or two pixels across a large area can still score. Layout Shift Regions will flash even when the movement is hard to notice.
- Sticky headers that shrink. A header that changes height as you scroll moves everything
below it. Keep its height fixed and animate with
transforminstead. - Lazy-loaded images further down. Lazy loading is fine for CLS as long as each image has its dimensions; the problem is a zero-height placeholder, not the lazy loading.
- Carousels. A carousel that moves slides by changing
leftormargincan register as shifts. Moving slides withtransformavoids this. - Back and forward navigation. Pages restored from the back/forward cache start a fresh measurement, so shifts on the original load are not repeated. Pages excluded from that cache load again and may shift again.
- Infinite scroll and "load more". New items appended below the viewport do not shift visible content. Items inserted above, or a footer that jumps as content loads, do.
Keeping CLS below 0.1
- Make dimensions part of your content rules. Every image, video and embed gets a known shape before it loads, whether it is added by a developer or an editor.
- Give every third-party slot a box. Ads, review widgets, chat launchers and embeds live in containers with a set minimum height or aspect ratio.
- Overlay, do not insert. Banners, notices and toasts sit on top of the page rather than pushing it down.
- Keep attribution reporting on. A new plugin, app or advert placement can bring CLS back overnight. Your own data will show the new selector within days.
- Check new templates with layout shift regions on a phone width and a slow connection before they go live.
For the scoring rules and a tool that lists every shift on a page, see the CLS guide and the CLS Debugger fixes linked above.
1. What is a good CLS score?
Google rates Cumulative Layout Shift as good at 0.1 or less, needs improvement from above 0.1 up to 0.25, and poor above 0.25. The rating uses the 75th percentile of real Chrome visits over 28 days, assessed separately for mobile and desktop. The number itself has no unit; it combines how much of the screen moved with how far it moved, and a page's score is its worst burst of shifts during the visit rather than the total of every shift.
2. Why is my CLS zero in Lighthouse but poor in Search Console?
Lighthouse only watches one page load on a simulated device and never scrolls or clicks. Real visitors scroll, stay on the page for minutes, see consent banners and trigger late-loading adverts and embeds, and CLS keeps being measured for the whole visit. Shifts that happen after load or further down the page therefore appear in field data but not in the lab test. Use real-user attribution with the web-vitals library to find the element responsible.
3. Does lazy loading images cause layout shift?
Not on its own. Lazy loading only delays when an image is downloaded. It causes layout shift when the image has no reserved space, so its placeholder is zero pixels tall until the file arrives and then suddenly grows. Give every lazy-loaded image width and height attributes or a CSS aspect ratio and the space is held from the start. Do not lazy-load the main image at the top of the page, but for a different reason: it slows Largest Contentful Paint.
4. Do animations count towards CLS?
Animations made with CSS transform, such as translate or scale, do not count, because they do not change the layout of the page. Animations that change properties such as top, left, margin, height or width do move other content and can be counted as layout shifts. If you have a sliding banner, expanding panel or moving carousel, switch it to transform-based movement. Changes within half a second of a click or tap are excluded anyway, since the visitor caused them.
5. Is CLS measured on mobile and desktop separately?
Yes. Google assesses mobile and desktop visits separately and Search Console has a report for each. CLS is often worse on mobile because the screen is narrow, so a shifting element covers a larger share of the viewport, and because layouts stack vertically, so anything that grows pushes everything below it down. A desktop page can pass while the same URL fails on phones. Always test at a narrow width when checking a fix.
6. How do I tell which element caused the shift rather than which one moved?
The browser reports the elements that moved, which are often innocent paragraphs or headings. The cause is usually whatever sits just above them and changed size or appeared. Turn on Layout Shift Regions in the Chrome DevTools Rendering tab, reload on a slow connection and watch what appears or grows at the moment of the blue flash. Comparing the page source with the rendered page also shows content that JavaScript added later.
7. How long does it take for a CLS fix to show up?
Your own tests and real-user reporting show the change as soon as the fix is live. The Chrome UX Report and Search Console use a rolling 28-day window of real visits, so the field score improves gradually as older visits drop out, and it can take close to four weeks to fully reflect the fix. Once every page in the affected group is fixed, start validation in the Search Console Core Web Vitals report and leave it to finish.
The Core Web Vitals checker shows real-user field data and a Lighthouse lab test side by side for your URL, breaks down each metric and points to the assets and code behind every failure, so you can confirm whether layout shift is what is failing.
Open Core Web Vitals Fixes โ