Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Minimize cumulative layout shift

Page maintains visual stability with a CLS score below 0.1, preventing unexpected content shifts during load.

Utilities
Quick take
Typical fix time 20 min
  • 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
Why it matters: Layout shifts cause accidental clicks, reading disruption, and user frustration—a good CLS score ensures content stays where users expect it.

Rule Details

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
import Image from 'next/image'
 
function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={600}
      priority
      // Dimensions prevent layout shift
    />
  )
}

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

ScoreRatingUser Experience
0–0.1GoodStable, no unexpected shifts
0.1–0.25Needs improvementNoticeable shifts
> 0.25PoorSignificant layout instability

Common Causes of Layout Shift

CauseImpactSolution
Images without dimensionsHighAlways set width/height
Ads and embedsHighReserve container space
Web fonts loadingMediumUse font-display: swap
Dynamic content injectionMediumUse placeholders
AnimationsLowUse 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
import { Inter } from 'next/font/google'
 
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  // Prevents layout shift from font loading
})
 
export default function RootLayout({ children }) {
  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 && <Banner />} {/* Pushes content down */}
      <Content />
    </div>
  )
}
 
// Good: Reserve space for banner
function Page() {
  const [showBanner, setShowBanner] = useState(false)
 
  return (
    <div>
      <div style={{ minHeight: '60px' }}>
        {showBanner && <Banner />}
      </div>
      <Content />
    </div>
  )
}

Measuring CLS

// Using web-vitals library
import { onCLS } from 'web-vitals'
 
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

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Measure CLS using Lighthouse or PageSpeed Insights. Verify score is below 0.1 for good visual stability.

Fix

Auto-fix issues

Reserve space for images/embeds with dimensions, use font-display strategies, and avoid injecting content above existing content.

Explain

Learn more

Explain how CLS measures visual stability and why unexpected layout shifts frustrate users and cause accidental clicks.

Review

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.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

PageSpeed Insights
pagespeed.web.devTool

Rules that often go hand-in-hand with this one.

Optimize first contentful paint

First content renders within 1.8 seconds, providing quick visual feedback that the page is loading.

Performance
Optimize largest contentful paint

The largest content element loads within 2.5 seconds for a good user experience.

Performance
Set explicit width and height on images

All <img> elements have explicit width and height attributes so browsers can reserve space before the image loads, preventing layout shift.

Images

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385