CLS Prevention: Visual Stability and Layout Shift Debugging
You're reading an article, you're about to click on a link — and suddenly everything moves 200 pixels downwards because an advertising banner has appeared above the text. You just experienced a Cumulative Layout Shift. The CLS and the Core Web metric Vitals which measures how much the contents of a page move unexpectedly during charging and use.
According to Google's CrUX data, approximately 27% of websites have a "Poor" CLS (above 0.25), which means that one in four users experiences it visually disturbing layout shifts on the most visited pages. Unlike of INP and LCP, CLS isn't about speed: it's about visual stability, an aspect of the user experience that directly impacts user frustration and trust in the site.
What You Will Learn
- How the CLS score is calculated and what "layout shift" means
- The Good/Needs Improvement/Poor thresholds and their SEO impact
- The five most common causes of CLS and how to eliminate them
- How to use Chrome DevTools and PerformanceObserver to debug shifts
- CSS pattern with aspect-ratio, min-height and content-visibility
- How to handle font swap, lazy loading and dynamic content without CLS
How CLS is Calculated
The CLS is not simply the number of shifts that occur: it is a composite score which takes into account both thebreadth of the shift (impact fraction) is of distance traveled by the element (distance fraction).
The formula is:
layout shift score = impact fraction * distance fraction
L'impact fraction and the percentage of the viewport involved in the shift. If an element that occupies 50% of the viewport height moves 25% of the height of the viewport, the impact fraction is 0.75 (the total area impacted before and after the shift). There distance fraction and the proportion of the viewport that the element travels during the shift.
The CLS Thresholds
| Rating | CLS value | User Experience |
|---|---|---|
| Good | < 0.1 | Excellent visual stability |
| Needs Improvement | 0.1 - 0.25 | Some perceptible shifts |
| Poor | > 0.25 | Unstable layout, frustrated users |
CLS is calculated by adding the scores of all layout shifts on the page, with one important exception: shifts caused by user interaction (scroll, click, input) are not counted. Only the shifts unexpected weigh on final score.
The Five Main Causes of CLS
1. Images and Media Without Explicit Sizes
The most frequent cause of CLS is uploading images without specifying width
e height. When the browser encounters a tag <img>
dimensionless, it does not reserve space in the layout. When the image loads, the
browser inserts the element into the DOM and all the underlying content moves towards it
the bass.
Wrong:
<img src="hero.jpg" alt="Hero image">
Correct:
<img src="hero.jpg" alt="Hero image" width="1200" height="630">
Specify width e height allows the browser to calculate
the aspect ratio of the image before uploading and reserve the correct space in the
layout. Modern CSS then manages responsive behavior:
img {
max-width: 100%;
height: auto; /* mantiene l'aspect ratio */
}
2. Web Fonts with Visually Different Fallback
When a web font is not yet downloaded, the browser displays the text with a font system (fallback). When the web font loads, the browser replaces the fallback causing a change in text size (so-called "CLS font swap"). The metric difference between a system font and a web font can be significant, causing shifts of hundreds of pixels on pages with a lot of text.
The modern solution and use size-adjust, ascent-override,
descent-override e line-gap-override to align metrics
of the fallback font to those of the web font:
/* Font fallback ottimizzato per Inter */
@font-face {
font-family: 'Inter-Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'Inter-Fallback', sans-serif;
}
This approach, combined with font-display: swap, reduces the CLS from
font almost to zero because the fallback has the same metric dimensions as the final font.
3. Dynamic Content Injected Over Existing Content
Cookie banners, pop-up newsletters, system notifications and advertisements that appearing on top of or within the content after loading are among the causes more frustrating than CLS. The problematic pattern is to inject an element into the DOM to a position that moves the already visible content.
The rule is simple: Always reserve space before content arrives:
/* Riservare spazio per un banner ad prima che sia caricato */
.ad-container {
min-height: 90px; /* altezza standard banner leaderboard */
width: 728px;
max-width: 100%;
/* Il banner si inserisce senza spostare nulla */
}
/* Cookie banner: posizionamento fisso non impatta il layout */
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
/* fixed non partecipa al flow del documento */
}
4. CSS Animations Using Properties That Trigger Layouts
Not all CSS animations cause layout shifts, but some do. The properties
top, left, right, bottom,
width, height, margin e padding
They trigger browser reflow (layout) and can cause CLS if they animate elements
that affect the layout of other elements.
The solution is to animate only the properties that do not trigger layout:
transform e opacity. These are managed by the composer
thread without involving the main thread.
/* Sbagliato: anima proprieta di layout */
.elemento-animato {
animation: slide-sbagliato 0.3s ease;
}
@keyframes slide-sbagliato {
from { left: -100px; }
to { left: 0; }
}
/* Corretto: usa transform */
.elemento-animato {
animation: slide-corretto 0.3s ease;
}
@keyframes slide-corretto {
from { transform: translateX(-100px); }
to { transform: translateX(0); }
}
5. Iframe Elements and Third Party Widgets Without Dimensions
Embed YouTube videos, Twitter widgets, Google maps and other third-party iframes they cause CLS if they do not have explicit dimensions. The behavior is identical to images: the browser doesn't know how much space to reserve.
<!-- Sbagliato -->
<iframe src="https://www.youtube.com/embed/..."></iframe>
<!-- Corretto: aspect-ratio moderno -->
<div class="video-container">
<iframe
src="https://www.youtube.com/embed/..."
title="Titolo del video"
allowfullscreen>
</iframe>
</div>
.video-container {
position: relative;
aspect-ratio: 16 / 9;
width: 100%;
}
.video-container iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
Debugging the CLS with Chrome DevTools
Performance Panel: Identify Layout Shifts
Chrome DevTools Performance panel is the most accurate tool for identify shift layouts. Follow these steps:
- Open DevTools and go to the tab Performance
- Click the register button (red circle)
- Reload the page or navigate to the section you want to analyze
- Stop recording after 5-10 seconds
- In the timeline, look for the purple bars labeled Layout Shift
- Click on a Layout Shift event to see the score and elements involved in the Summary section
Rendering Panel: Layout Shift Regions
An alternative way is to use the Render panel. Open DevTools, press
Escape to open the drawer, select the "Rendering" tab e
activate the option "Layout Shift Regions". The browser highlights
in blue every area that undergoes a shift as you navigate the page.
PerformanceObserver for Production Monitoring
To collect real CLS data from users in production, use the
PerformanceObserver API or the library web-vitals:
import { onCLS } from 'web-vitals';
onCLS((metric) => {
console.log('CLS finale:', metric.value, metric.rating);
// Le singole shift entries per il debugging
metric.entries.forEach((entry) => {
console.log('Shift:', {
score: entry.value,
elements: entry.sources?.map((s) => s.node),
startTime: entry.startTime,
});
});
// Invia a analytics
fetch('/api/vitals', {
method: 'POST',
body: JSON.stringify({
metric: 'CLS',
value: metric.value,
rating: metric.rating,
}),
});
});
Advanced CSS Patterns to Eliminate CLS
aspect-ratio: The Modern Replacement for the Padding-Bottom Hack
Before aspect-ratio, the way to preserve the aspect ratio
of a container was the "padding-bottom hack". Today aspect-ratio
and available in all modern browsers (Baseline 2021) and offers a solution
clean and readable:
/* Vecchio approccio: padding-bottom hack */
.video-wrapper-old {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
}
/* Approccio moderno: aspect-ratio */
.video-wrapper {
aspect-ratio: 16 / 9;
width: 100%;
}
/* Funziona anche per card immagini */
.card-thumbnail {
aspect-ratio: 3 / 2;
overflow: hidden;
}
.card-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
content-visibility: lazy for Off-Screen Sections
The CSS property content-visibility: auto instructs the browser to
skip rendering of sections that are not in the viewport. Combined with
contain-intrinsic-size, allows the browser to estimate the size
of the element even before rendering it, avoiding shift when the section
enter the viewport:
.section-offscreen {
content-visibility: auto;
/* Altezza stimata della sezione */
contain-intrinsic-size: 0 800px;
}
Skeleton Screen: Reserve Space for Asynchronous Content
For content loaded asynchronously (data from API, comments, recommendation widgets), uses a skeleton screen that replicates the structure of the final content:
/* Skeleton base */
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.skeleton-title {
height: 24px;
width: 70%;
margin-bottom: 8px;
}
.skeleton-text {
height: 16px;
width: 100%;
margin-bottom: 6px;
}
.skeleton-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
}
CLS in Frameworks: Angular, React, Vue
Angular: Handle CLS during Hydration
With Angular SSR, the hydration phase (when Angular "comes to life" in the HTML
server-rendered) can cause shifts if components modify the DOM in such a way
which creates differences between the HTML server-side and what Angular produces client-side.
The solution is to make sure that ngSkipHydration be used only for
components that really can't do hydration, and that all components that
show content above-the-fold have a fixed size.
Lazy Loading Images: fetchpriority="high" for LCP + loading="lazy" for the Rest
A common pattern that causes involuntary CLS to apply loading="lazy"
to all images, including the LCP one. Lazy loading delays loading
of the image, but the browser still has to reserve space — if they aren't there
explicit dimensions, you have CLS when the image is finally loaded.
<!-- Immagine hero (above the fold): non usare lazy -->
<img
src="hero.webp"
alt="Hero image"
width="1200"
height="600"
fetchpriority="high"
>
<!-- Immagini below the fold: lazy loading + dimensioni esplicite -->
<img
src="product-1.webp"
alt="Prodotto 1"
width="400"
height="300"
loading="lazy"
>
CLS and Single Page Applications
In SPAs (React, Angular, Vue), client-side navigation can introduce CLS if the new "page" has elements with different sizes than the previous ones. The View Transitions API (available in Chrome and Firefox) offers a solution elegant by animating the state change instead of doing a hard replace of the DOM. See the dedicated article in the series for the complete implementation.
CLS Audit: Practical Checklist
Use this checklist to audit your site's CLS:
- All pictures have
widtheheightexplicit? - Iframes and video embeds have fixed sizes or use
aspect-ratio? - Web fonts use
font-display: swapwith optimized fallback metrics? - Do advertising banners have a container with a reserved minimum size?
- Animations use
transformeopacityinstead of layout properties? - Does dynamic content (from API, comments) use skeleton screen or placeholder?
- Popups and banner cookies use
position: fixedoposition: absolute? - Critical web fonts are preloaded in the
<head>?
Measuring CLS at Scala: Lighthouse CI and CrUX
To monitor CLS systematically on a site with many pages, integrate Lighthouse CI in your deployment pipeline:
# lighthouserc.json
{
"ci": {
"assert": {
"assertions": {
"cumulative-layout-shift": ["error", {"minScore": 0.9}]
}
},
"collect": {
"url": [
"http://localhost:4200",
"http://localhost:4200/products",
"http://localhost:4200/blog"
],
"numberOfRuns": 3
}
}
}
# GitHub Actions: esegui Lighthouse CI su ogni PR
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v11
with:
configPath: ./lighthouserc.json
temporaryPublicStorage: true
Next Steps
With INP, LCP, and CLS covered, you now have a complete understanding of the three metrics
Core Web Vitals by Google. The next step is to delve deeper into the optimization of the
font: the next article in the series explores font subsetting, variable fonts and
the strategies font-display to maximize LCP performance e
minimize CLS from font swap.
Conclusions
CLS is a metric that requires systematic attention throughout the process development, not just as post-hoc fixes. Winning patterns are simple and consistent: explicit dimensions for each visual asset, reserved space for dynamic content, animations that don't trigger layouts, fallback fonts with aligned metrics.
The good news is that solving CLS is often easier than solving INP or LCP:
requires no architectural refactoring, just the disciplined application of
these CSS and HTML patterns. A methodical audit with Chrome DevTools and the library
web-vitals will allow you to bring your site's CLS into range
"Good" in one or two work sessions.







