How to Debug a Jumping UI to Fix CLS
Cumulative Layout Shift (CLS) measures visual instability. Learn how to find and fix the elements causing your page to jump and shift.
Nothing frustrates users more than clicking a button only to have the page shift and clicking something else entirely. Cumulative Layout Shift (CLS) measures this visual instability, and Google uses it as a ranking factor. A "Good" CLS score is under 0.1.
Finding Layout Shifts
Chrome DevTools Performance Tab
- Open DevTools → Performance tab
- Check "Screenshots" and record a page load
- Look for red dashed lines labeled "Layout Shift"
- Click each shift to see which element moved and by how much
Web Vitals Extension
Install the "Web Vitals" Chrome extension. It shows CLS in real-time and highlights shifted elements with a blue overlay.
JavaScript API
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log('Layout shift:', entry.value, entry.sources);
entry.sources?.forEach(source => {
console.log(' Shifted element:', source.node);
console.log(' Previous rect:', source.previousRect);
console.log(' Current rect:', source.currentRect);
});
}
}
}).observe({ type: 'layout-shift', buffered: true });
Top 6 CLS Causes and Fixes
1. Images Without Dimensions
Cause: Browser doesn't know image size until it downloads, so content below shifts when the image loads.
<!-- BAD: No dimensions → layout shift -->
<img src="/hero.jpg" alt="Hero">
<!-- GOOD: Dimensions reserve space -->
<img src="/hero.jpg" alt="Hero" width="1200" height="800">
For CSS-sized images:
/* Reserve space with aspect-ratio */
.hero-img {
width: 100%;
aspect-ratio: 3 / 2;
object-fit: cover;
}
2. Ads and Embeds Without Reserved Space
Ad slots resize when ads load. Reserve space:
.ad-slot {
min-height: 250px; /* Common ad height */
background: #1a1a2e; /* Placeholder color */
display: flex;
align-items: center;
justify-content: center;
}
3. Web Fonts Causing Text Reflow
When a web font loads, text resizes if the fallback font has different metrics:
/* Use font-display: swap with adjusted fallback */
@font-face {
font-family: 'Heading';
src: url('/heading.woff2') format('woff2');
font-display: swap;
}
/* Use size-adjust on fallback to match web font metrics */
@font-face {
font-family: 'Heading-Fallback';
src: local('Arial');
size-adjust: 105%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
h1 { font-family: 'Heading', 'Heading-Fallback', sans-serif; }
4. Dynamically Injected Content
Content inserted above existing content pushes everything down:
// BAD: Inserts banner at top, shifts everything
document.body.prepend(bannerElement);
// GOOD: Reserve space for the banner
// CSS: .banner-slot { min-height: 60px; }
document.querySelector('.banner-slot').appendChild(bannerElement);
5. Animations Triggering Layout
Animations using top, left, width, height, or margin cause layout shifts:
/* BAD: triggers layout */
.menu { transition: height 0.3s; }
.menu.open { height: 300px; }
/* GOOD: use transform (doesn't trigger layout) */
.menu {
transform: scaleY(0);
transform-origin: top;
transition: transform 0.3s;
}
.menu.open { transform: scaleY(1); }
6. Late-Loading Components
React components that load data and then render cause shifts:
// BAD: No skeleton → content jumps in
function Feed() {
const { data } = useSWR('/api/feed');
if (!data) return null; // Empty space until loaded
return <FeedList items={data} />;
}
// GOOD: Skeleton reserves space
function Feed() {
const { data } = useSWR('/api/feed');
if (!data) return <FeedSkeleton />; // Same dimensions as FeedList
return <FeedList items={data} />;
}
CLS Debugging Checklist
- All images have
widthandheightattributes - All videos and iframes have dimensions
- Ad slots have reserved
min-height - Web fonts use
font-display: swapwith adjusted fallbacks - No content is injected above existing content
- Animations use
transformandopacityonly - Loading states use skeletons matching final dimensions
- Cookie banners and notification bars have reserved space
Monitor CLS Continuously
CLS can regress with any content change. BadPageSpeed tracks CLS across all your pages so you catch shifts immediately.
Ready to stop wasting ad spend?
Track your landing page performance, monitor Core Web Vitals, and calculate exactly how much slow pages cost you.
Start Free — No Credit Card