
Cumulative Layout Shift
- 1 installs
- 73.4k repo stars
- Updated June 18, 2026
- thedaviddias/frontendchecklist
Measures CLS and reserves space for images, embeds, and dynamic content to keep the layout visually stable below 0.1.
About
A frontend-checklist performance rule for minimizing Cumulative Layout Shift. A developer uses it when shifting content causes misclicks and reading disruption.
- Set width/height on images and reserve space for dynamic content
- Target a CLS score below 0.1
Cumulative Layout Shift by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,914 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedaviddias/frontendchecklist --skill cumulative-layout-shiftAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 73.4k |
| Last updated | June 18, 2026 |
| Repository | thedaviddias/frontendchecklist ↗ |
What it does
Measures CLS and reserves space for images, embeds, and dynamic content to keep the layout visually stable below 0.1.
Files
Minimize cumulative layout shift
Layout shifts cause accidental clicks, reading disruption, and user frustration—a good CLS score ensures content stays where users expect it.
Quick Reference
- CLS measures visual stability—target score below 0.1
- Always set width/height on images and embeds
- Reserve space for ads, banners, and dynamic content
- Use transform animations instead of layout-changing properties
Check
Measure CLS using Lighthouse or PageSpeed Insights. Verify score is below 0.1 for good visual stability.
Fix
Reserve space for images/embeds with dimensions, use font-display strategies, and avoid injecting content above existing content.
Explain
Explain how CLS measures visual stability and why unexpected layout shifts frustrate users and cause accidental clicks.
Code Review
Review the routes, assets, and loading behavior that affect Minimize cumulative layout shift. Flag exact files, requests, or rendering steps that add unnecessary network, CPU, or layout cost, and describe the measurement method used to confirm the issue.
---
For full implementation details, code examples, and framework-specific guidance, see references/rule.md.
Rule page: https://frontendchecklist.io/en/rules/performance/cumulative-layout-shift
Minimize cumulative layout shift
Page maintains visual stability with a CLS score below 0.1, preventing unexpected content shifts during load.
Priority: high · Difficulty: intermediate · Time: 20 min
--- Cumulative Layout Shift measures unexpected movement of page content.
Code Examples
<!-- Always specify width and height -->
<img
src="hero.jpg"
alt="Hero image"
width="1200"
height="600"
loading="lazy"
>
<!-- Or use aspect-ratio CSS -->
<img
src="hero.jpg"
alt="Hero image"
style="aspect-ratio: 16/9; width: 100%; height: auto;"
>// React/Next.js with automatic dimensions
function Hero() {
return (
)
}Why It Matters
Layout shifts cause accidental clicks, reading disruption, and user frustration—a good CLS score ensures content stays where users expect it.
CLS Score Thresholds
| Score | Rating | User Experience |
|---|---|---|
| 0–0.1 | Good | Stable, no unexpected shifts |
| 0.1–0.25 | Needs improvement | Noticeable shifts |
| > 0.25 | Poor | Significant layout instability |
Common Causes of Layout Shift
| Cause | Impact | Solution |
|---|---|---|
| Images without dimensions | High | Always set width/height |
| Ads and embeds | High | Reserve container space |
| Web fonts loading | Medium | Use font-display: swap |
| Dynamic content injection | Medium | Use placeholders |
| Animations | Low | Use transform/opacity |
Reserve Space for Dynamic Content
// Reserve space for ads
function AdBanner() {
return (
<div
style={{
minHeight: '250px',
width: '300px',
backgroundColor: '#f0f0f0',
}}
>
{/* Ad loads here without causing shift */}
</div>
)
}
// Skeleton loading for content
function ArticleCard({ isLoading, article }) {
if (isLoading) {
return (
<div className="article-card">
<div className="skeleton" style={{ height: '200px' }} />
<div className="skeleton" style={{ height: '24px', width: '80%' }} />
<div className="skeleton" style={{ height: '16px', width: '60%' }} />
</div>
)
}
return (
<div className="article-card">
<img src={article.image} alt={article.title} width={300} height={200} />
<h2>{article.title}</h2>
<p>{article.excerpt}</p>
</div>
)
}Font Loading Strategy
/* Use font-display to prevent FOIT/FOUT shifts */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when loaded */
size-adjust: 100%; /* Match fallback font metrics */
ascent-override: 90%;
descent-override: 20%;
}
/* Use similar fallback font */
body {
font-family: 'CustomFont', Arial, sans-serif;
}// Next.js font optimization
const inter = Inter({
subsets: ['latin'],
display: 'swap',
// Prevents layout shift from font loading
})
return (
<html className={inter.className}>
<body>{children}</body>
</html>
)
}Animations Without Layout Shift
/* Bad: Animating layout properties */
.bad-animation {
animation: slide-bad 0.3s ease-out;
}
@keyframes slide-bad {
from { margin-left: -100px; } /* Causes layout shift */
to { margin-left: 0; }
}
/* Good: Using transform */
.good-animation {
animation: slide-good 0.3s ease-out;
}
@keyframes slide-good {
from { transform: translateX(-100px); } /* No layout shift */
to { transform: translateX(0); }
}Avoid Content Injection Above Fold
// Bad: Inserting banner above content
function Page() {
const [showBanner, setShowBanner] = useState(false)
useEffect(() => {
// This causes layout shift when banner appears
setTimeout(() => setShowBanner(true), 1000)
}, [])
return (
<div>
{showBanner && } {/* Pushes content down */}
</div>
)
}
// Good: Reserve space for banner
function Page() {
const [showBanner, setShowBanner] = useState(false)
return (
<div>
<div style={{ minHeight: '60px' }}>
{showBanner && }
</div>
</div>
)
}Measuring CLS
// Using web-vitals library
onCLS(metric => {
console.log('CLS:', metric.value)
// Report to analytics
if (metric.value > 0.1) {
console.warn('CLS exceeds threshold', metric.entries)
}
})
// Debug which elements caused shifts
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue // Ignore user-triggered shifts
console.log('Layout shift:', {
value: entry.value,
sources: entry.sources?.map(s => ({
node: s.node,
currentRect: s.currentRect,
previousRect: s.previousRect
}))
})
}
}).observe({ type: 'layout-shift', buffered: true })Verification
Automated Checks
- Run Lighthouse—check CLS score in Performance
- Use Chrome DevTools Performance panel
- Check PageSpeed Insights for field data
Manual Checks
- Test on slow connections (reveals timing-based shifts)
- Monitor with Real User Monitoring