CLS measures how much page content unexpectedly shifts during loading. Good CLS (under 0.1) signals a stable, predictable experience. Poor CLS (over 0.25) signals an experience where users click on the wrong things because the page kept moving under them.
CLS is one of the three Core Web Vitals Google uses for ranking. The metric became a ranking factor in 2021 and was updated in 2022 to focus on a one-second window of the worst sequential shifts rather than the lifetime score. By 2026, CLS is part of the page experience signal Google has publicly confirmed, with stronger weighting on sites where users complete forms, click navigation items, or interact with content during the initial render.
The map: what CLS actually measures, the recurring causes across most sites, and the fixes that consistently produce improvement.
What CLS measures:
CLS records the cumulative score of unexpected layout shifts during the page’s lifetime, focused on the worst one-second window of shifts. Each shift is scored as:
Shift score = impact fraction × distance fraction
Where impact fraction is the share of the viewport affected by the shift, and distance fraction is how far elements moved relative to the viewport dimension. A large element moving a short distance scores similarly to a small element moving a long distance.
The thresholds:
| Performance | CLS value |
|---|---|
| <strong>Good</strong> | ≤ 0.1 |
| <strong>Needs improvement</strong> | 0.1 – 0.25 |
| <strong>Poor</strong> | > 0.25 |
The thresholds apply to the 75th percentile of page loads. A site with median CLS of 0.05 but a 75th percentile of 0.3 is rated poor.
What counts as “unexpected” matters. Shifts that happen within 500ms of a user action (clicking, typing, scrolling) are excluded from CLS scoring; those shifts are presumed to be user-triggered and acceptable. Shifts that happen during page load with no user action are counted; those are the shifts users complain about.
The recurring causes:
The same handful of causes account for most CLS problems across the sites where the metric gets diagnosed.
The first cause is images without explicit dimensions. An image without width and height attributes (or aspect-ratio CSS) takes zero space in the layout until it loads. When it loads, it pushes everything below it down. The fix is the oldest in web development: specify dimensions.
Second comes ads injected during page load. Display ads, especially those that load programmatically through ad servers, reserve no space before they appear. When the ad renders, the page shifts to make room. Fixing this requires either reserving the space upfront with placeholder elements at the correct dimensions, or accepting that ads will cause shifts and minimizing their frequency.
Embedded widgets and iframes come third. Social media embeds (X posts, Instagram, YouTube), comment widgets (Disqus), live chat widgets, and analytics-driven UI all share the pattern: no space reserved, content injected during load, layout shifts.
Web fonts swapping in are the fourth recurring cause. The FOUT (Flash of Unstyled Text) and FOIT (Flash of Invisible Text) patterns both cause shifts. Text rendered in a fallback font has different metrics than text rendered in the web font; when the web font arrives, lines reflow.
The fifth recurring cause is dynamically loaded content above existing content. A banner, a notification, or a hero element loaded by JavaScript and inserted at the top of the page pushes everything below down. Common in sites with personalization or geo-targeted banners.
Finally, animations and transitions on layout-affecting properties. CSS animations that change width, height, top, left, or margin cause layout recalculation. The visual effect that the developer wanted produces shifts the user perceives as instability.
Image dimension fixes:
The simplest CLS fix is also the most universally applicable: every image gets explicit dimensions.
HTML approach (modern browsers compute aspect ratio from width/height):
<img src="hero.webp" width="1200" height="600" alt="..." />
CSS approach (for background images or when HTML dimensions don’t apply):
.hero {
aspect-ratio: 2 / 1;
background-image: url(hero.webp);
}
The pattern works because the browser reserves space at the correct aspect ratio before the image loads. When the image arrives, it fills the reserved space without pushing other content.
For responsive images using srcset, the dimensions in width/height attributes should match the intrinsic dimensions of the default src; browsers compute aspect ratio from these and apply it across the srcset variants.
For images where the dimensions vary by viewport (different crops on mobile vs desktop), the picture element with multiple source variants, each with its own dimensions, handles the cases. The browser picks the right source and reserves space accordingly.
Ad space reservation:
For ad placements that cause CLS, two strategies work.
Reserve fixed dimensions for the ad slot. The container has explicit width and height matching the ad’s expected size. The ad loads into the container without changing the surrounding layout. Drawback: if the ad doesn’t load, the empty space sits there.
The other approach uses a min-height with content centered. The container has minimum height matching the largest expected ad size. If a smaller ad loads, the centered content stays stable; if no ad loads, the space is occupied by other content (a placeholder, a related article, anything).
For programmatic ad networks where the ad size varies, the strategy is to constrain the variation to a fixed set of sizes and reserve space for the largest. The trade-off accepts some whitespace for the smaller ads in exchange for layout stability.
Widget and iframe handling:
Embedded content that doesn’t declare its size causes shifts when it loads. The fixes:
- Use the embed code as the provider documents it. Most major embed services (YouTube, Vimeo, X, Instagram) now include dimension hints in their official embed code. Using their official code typically avoids CLS issues.
- Wrap the embed in a container with reserved space. If the official embed lacks dimensions, wrap it in a div with explicit width and height (or aspect-ratio).
- Lazy-load embeds with placeholder content. Until the user scrolls near the embed, show a placeholder at the correct dimensions. When the embed enters the viewport, it loads into the reserved space.
For chat widgets and notification bars that appear at the top or bottom of the page, the fix is positioning them absolutely or fixed rather than allowing them to push other content. The widget appears over the existing layout rather than displacing it.
Web font loading strategies:
The font-display CSS property controls how browsers handle missing web fonts:
| Value | Behavior | CLS impact |
|---|---|---|
| <strong>auto</strong> (default) | Browser default, usually similar to block | Variable, often poor |
| <strong>block</strong> | Hide text until font loads (FOIT) | Low CLS but invisible text |
| <strong>swap</strong> | Show fallback, swap when font arrives | Higher CLS as font swaps |
| <strong>fallback</strong> | Brief block, then fallback, then swap if quick | Moderate CLS |
| <strong>optional</strong> | Block briefly, use fallback if font isn't ready | Minimal CLS |
For most sites, optional produces the best balance: brief font blocking, then fallback fonts (no shift), then web fonts only if they’re ready quickly. The trade-off is that users on slow connections may not see the web font at all on their first visit.
Beyond font-display, font matching improves CLS substantially. The size-adjust, ascent-override, descent-override, and line-gap-override descriptors in @font-face let developers match the fallback font’s metrics to the web font’s metrics. When metrics match, the swap from fallback to web font produces no shift because the text occupies the same space.
A typical implementation:
@font-face {
font-family: "Inter";
src: url("inter.woff2") format("woff2");
font-display: optional;
size-adjust: 100%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
The values are font-specific; tools like Font Style Matcher help calculate the right overrides for matching a fallback font (like system-ui or Arial) to the target web font.
Dynamic content placement:
Banners, notifications, and personalized content loaded after initial render cause shifts unless space is reserved or the content is positioned outside the document flow.
The patterns that work:
- Position fixed or absolute for elements that don’t need to push other content. The notification appears at the top of the viewport but doesn’t displace the page below.
- Reserve space conditionally. If the personalization logic might add a banner, reserve the space for it. If the banner doesn’t load, fill the space with default content.
- Insert dynamic content below the fold. Content added below the user’s current viewport doesn’t shift visible content (no CLS impact for off-screen shifts).
- Server-render personalized content so the initial HTML response includes the right state for each user. The personalization logic runs server-side; no client-side insertion happens during load.
The pattern to avoid: client-side JavaScript that loads content and inserts it above existing content. The shift is visible, often significant, and reliable enough to register at the 75th percentile.
Animation and transition discipline:
Animations on layout-affecting properties cause CLS. The fix is using GPU-accelerated properties that don’t trigger layout:
| Animate this | Not this |
|---|---|
| <strong>transform</strong> (translate, scale, rotate) | top, left, right, bottom, margin |
| <strong>opacity</strong> | visibility (when combined with size changes) |
| <strong>filter</strong> | width, height (use scale instead) |
The principle: properties that the browser can apply on the GPU (transform, opacity, filter) don’t require the layout engine to recalculate positions for other elements. Properties that change layout (width, height, margin, top/left positioning) trigger the layout engine and cause shifts.
For animations that need to change perceived size or position, scale and translate produce the same visual effect as width/height/margin changes without the CLS cost.
Diagnostic workflow:
The workflow that consistently finds CLS issues:
Start with field data from Search Console’s Core Web Vitals report. The report shows URLs grouped by CLS performance. The poor and needs-improvement groups are the starting points.
Move to PageSpeed Insights for representative URLs. PSI shows the field data plus a lab Lighthouse run. The Lighthouse report identifies specific elements responsible for shifts during the test.
For the most detailed view, open the Chrome DevTools Performance panel with the Web Vitals overlay. Running a recording during page load shows every layout shift with timestamps, the elements involved, and the shift score.
For ongoing visibility, set up real-user monitoring through services like SpeedCurve, Cloudflare Browser Insights, or New Relic. Field data surfaces shifts that lab tests might miss (third-party content that varies per load, geographic differences, device-specific issues).
The mobile vs desktop split:
CLS on mobile tends to be higher than on desktop for several reasons:
- Smaller viewports mean shifts have larger impact fractions. A 50-pixel shift takes up more of a 375-pixel viewport than a 1920-pixel one.
- Ads and widgets often respond differently on mobile. Sizes change, formats change, behavior varies.
- Mobile network variability means resources arrive at different times, causing more visible shift patterns.
The optimization priority follows the same logic as LCP: mobile first. A site with good mobile CLS usually has good desktop CLS too; the reverse isn’t reliably true.
CLS in single-page applications and route transitions:
CLS measures shifts during the page lifecycle, which behaves differently in SPAs than in traditional multi-page sites. Two patterns matter:
Initial load CLS. Same mechanics as multi-page sites; the SPA’s initial HTML and JavaScript hydration produce shifts the same way other pages do. The standard CLS optimizations apply: reserve space for images, ads, embeds, fonts.
Route transition CLS (session windows). When users navigate within an SPA (clicking a link that triggers client-side routing rather than a full page reload), the new view replaces the old view in the same document. Layout shifts during this transition count toward CLS in session windows, which Google uses for measurement. A poorly designed route transition that shifts the entire layout while content loads can produce poor CLS scores even when initial load CLS was good.
The fixes for route transition CLS:
- Reserve space for the incoming view. Skeleton screens that match the eventual layout prevent shifts when real content arrives.
- Use view transitions API where supported. The CSS View Transitions API (supported in Chromium-based browsers as of 2024) lets developers define smooth transitions between views without layout shift. Browser support is still partial; the technique is progressive enhancement.
- Prefetch and preload route data. SPAs that fetch data on navigation produce loading states; SPAs that prefetch data when users hover over links produce instant transitions without intermediate shifts.
Modern SPA frameworks (Next.js App Router, Remix, SvelteKit) handle some of this automatically through their routing layers. Custom routing implementations need explicit attention to transition design.
Monitoring CLS in production:
CLS is one of the metrics where lab data and field data diverge most dramatically. Lab testing (Lighthouse, PageSpeed Insights synthetic) tests on a clean browser with no third-party scripts loaded, no advertising state, and no user interaction. Production users see different content, including ads that load, embeds that initialize, A/B test variants that swap, and personalization that changes layout.
The monitoring stack:
- Chrome User Experience Report (CrUX). Google’s public dataset of real-user metrics. CrUX shows 75th percentile CLS across the trailing 28 days for sufficiently popular pages. The CrUX dashboard (g.co/crux) and PageSpeed Insights both surface this data. CrUX is the dataset Google uses for ranking signal calculations.
- Real-User Monitoring (RUM) platforms. Cloudflare Browser Insights, New Relic Browser, Datadog RUM, SpeedCurve, and Sentry Performance all collect Core Web Vitals data from real users. The advantage over CrUX is granularity: per-page, per-template, per-traffic-source breakdowns. The cost is the monitoring service subscription.
- Web Vitals JavaScript library. Google’s open-source
web-vitalslibrary (npm:web-vitals) collects CLS and other Core Web Vitals data directly from the browser. Sites integrate it with their analytics platform (Google Analytics, custom data warehouse) for in-house monitoring. - CI/CD performance budgets. Lighthouse CI integrated into deployment pipelines catches CLS regressions before they reach production. The catch is that CI tests don’t reproduce production conditions (ads, A/B tests, personalization), so they catch some regressions and miss others.
The discipline that works: combine CrUX (for the Google-perceived metric), RUM (for granular real-user data), and CI/CD performance budgets (for regression prevention). The three together catch most CLS issues; relying on any one in isolation misses important cases.
Common mistakes:
Patterns that recur:
- Fixing lab CLS without monitoring field data. Lighthouse runs are deterministic; real users experience variable conditions that produce different shift patterns.
- Adding fixed dimensions but not verifying aspect ratio. A 4:3 image displayed as 16:9 produces letterboxing that may look wrong even if CLS is fixed.
- Reserving too much space. Empty space at the top of the page (for ads that don’t load, for banners that don’t appear) damages user experience even when CLS is good.
- Treating animation-related shifts as unavoidable. Most animations can be rewritten to use transform/opacity instead of layout-affecting properties.
- Ignoring third-party CLS. Third-party scripts (analytics, ads, widgets) can cause shifts the team didn’t write. The fix may involve removing the third party, replacing it, or working with the provider to fix their implementation.
CLS as architectural property, not periodic fix:
CLS optimization is mostly mechanical: identify the shifts, identify the elements causing them, reserve space or change properties. The complexity comes from third-party content that the team doesn’t control directly and from the discipline of maintaining CLS as the site changes over time.
The pattern that produces durable good CLS: dimension specification as a build-time requirement (every image gets dimensions, every embed gets reserved space, every ad slot gets explicit size), regression testing in CI that catches CLS degradations before deployment, and field data monitoring that detects drift from real users.
The sites with good CLS treat it as a property of the site’s HTML and CSS architecture, not as a thing to fix periodically. The sites that fix it periodically end up fixing the same issues again, every time new templates ship or new third parties get added.