Fix CLS in WordPress
Cumulative Layout Shift (CLS) measures the total amount of unexpected visual movement on a page during its lifetime. A score of 0 means nothing moves after the initial paint; a score above 0.1 means users will perceive jarring jumps as they read or interact with the page. WordPress sites are particularly susceptible to CLS because they aggregate content from many sources: themes, plugins, ad networks, embedded media, and web fonts -- each of which can inject unsized content that shifts the layout as it loads. A score of 0.34 means users are experiencing significant shifts, making it difficult to click the correct link or read text without losing their place. The five fixes below address the highest-impact CLS causes in WordPress, moving the score from 0.34 to 0.05, which is within Google's Good threshold of 0.1.
Expected results
Before
0.34
CLS (Poor) -- unsized images, font swap shifts, ads without reserved space
After
0.05
CLS (Good) -- explicit dimensions, font-display:swap, contained ad slots
Step-by-step fix
Add width and height to all images
Images without width and height attributes cause the browser to allocate zero height for the image container until the image file has downloaded enough bytes to determine its dimensions. This causes all content below the image to jump downward when the image expands to its natural height. WordPress's wp_get_attachment_image() function outputs the correct dimensions automatically, but many themes build <img> tags manually or via custom HTML. The wp_img_tag_add_width_and_height_attr filter, introduced in WordPress 5.5, attempts to inject dimensions retroactively on any image tag missing them in the HTML output.
<?php
// functions.php
// Correct approach: always use wp_get_attachment_image() in theme templates
// This automatically outputs width, height, srcset, and sizes
$image_html = wp_get_attachment_image(
$attachment_id,
'large', // registered image size
false, // not an icon
array(
'class' => 'hero-image',
'loading' => 'lazy', // use 'eager' for LCP image
'alt' => get_the_title(),
)
);
// Outputs: <img src="..." width="1024" height="512" srcset="..." sizes="...">
// Filter to auto-add missing dimensions to any <img> tag in page output
// WordPress 5.5+ tries this automatically, but you can hook it for custom sources:
add_filter( 'wp_img_tag_add_width_and_height_attr', function( $value, $image, $context, $attachment_id ) {
// Return true to force dimension injection even in 'other' context
if ( $attachment_id ) {
return true;
}
return $value;
}, 10, 4 );
// For content images (post body), WordPress 5.5+ handles dimensions via
// wp_filter_content_tags() which runs on the_content and the_excerpt.
// Ensure this is not disabled in your theme or plugins:
// remove_filter( 'the_content', 'wp_filter_content_tags' ); // <-- remove this if present
Reserve space for ads and embeds
Ad units, YouTube embeds, Twitter cards, and other third-party iframes have dimensions that are unknown to the browser until the third party responds. Without reserved space, these elements collapse to zero height in the initial layout and then expand to their full size when the content arrives, pushing all subsequent page content down. The CSS aspect-ratio property is the modern solution: set it on the container along with a known width, and the browser calculates and reserves the correct height before any content loads. The Ad Inserter plugin allows setting minimum dimensions per ad position, which serves the same purpose for programmatic ads.
/* CSS: reserve space for common ad sizes */
/* 728x90 leaderboard */
.ad-leaderboard {
width: 100%;
max-width: 728px;
aspect-ratio: 728 / 90;
min-height: 90px;
background: var(--ad-placeholder-bg, #f3f4f6);
overflow: hidden;
}
/* 300x250 medium rectangle (sidebar) */
.ad-rectangle {
width: 300px;
aspect-ratio: 300 / 250;
min-height: 250px;
overflow: hidden;
}
/* Responsive video embeds (YouTube, Vimeo) */
.embed-responsive {
position: relative;
aspect-ratio: 16 / 9;
width: 100%;
overflow: hidden;
}
.embed-responsive iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}
/* Ad Inserter plugin: Block settings
Appearance tab > set "Min Height" to the ad unit's expected height
This outputs a placeholder div before the ad script executes */
Preload web fonts with font-display:swap
When a browser cannot download a web font quickly enough, it either shows invisible text (FOIT -- Flash of Invisible Text) or renders fallback text and then swaps to the custom font when it loads (FOUT -- Flash of Unstyled Text). Both behaviors cause CLS if the two fonts differ significantly in character width, causing lines to reflow. The font-display: swap descriptor instructs the browser to render fallback text immediately and swap to the custom font when it arrives, paired with size-adjust to match the fallback font metrics and minimize the shift. Adding a rel="preload" link for critical font files reduces the time window in which FOUT can occur.
<?php
// functions.php
// 1. Enqueue Google Fonts with font-display=swap via URL parameter
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_style(
'google-fonts',
'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap',
array(),
null
);
} );
// 2. Preload self-hosted font files for zero-CLS font loading
add_action( 'wp_head', function() {
$font_url = get_theme_file_uri( 'fonts/inter-regular.woff2' );
printf(
'<link rel="preload" href="%s" as="font" type="font/woff2" crossorigin="anonymous">' . PHP_EOL,
esc_url( $font_url )
);
}, 1 );
// 3. Self-hosted font @font-face with font-display:swap in theme CSS
/*
@font-face {
font-family: 'Inter';
src: url('../fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
/* size-adjust matches fallback Arial metrics to reduce reflow *\/
size-adjust: 107%;
ascent-override: 90%;
}
*/
// For Google Fonts loaded via wp_enqueue_style, add display=swap to the URL.
// The &display=swap query parameter is appended automatically to the URL above.
// Verify: view page source and confirm font URL contains &display=swap
Identify and disable layout-shifting plugins
WordPress plugins can contribute to CLS through cookie consent banners that push page content down on first visit, newsletter popups that shift the layout when they animate in, admin bars visible to logged-in users on the front-end, floating chat widgets that resize the page, and dynamically injected content blocks that load after the initial paint. Identifying which plugin is responsible requires Chrome DevTools. Once you know which CSS selector or script is causing the shift, you can either dequeue that plugin's resources on front-end pages, replace the plugin with a CLS-friendly alternative, or configure the plugin to use fixed positioning so it does not affect document flow.
<?php
// functions.php
// Step 1: Identify shifting plugins using Chrome DevTools
// DevTools > More Tools > Rendering > Layout Shift Regions
// Blue highlights show shifting elements on page load
// Step 2: Match the element to a plugin via Sources panel
// Right-click a highlighted element > Inspect
// Check which stylesheet defines its positioning rules
// Step 3: Dequeue the offending plugin's CSS for non-admin users
add_action( 'wp_enqueue_scripts', function() {
if ( ! is_admin() ) {
// Example: remove a newsletter plugin's stylesheet from the front-end
// Replace 'newsletter-plugin-style' with the actual handle
// Find handles via Query Monitor plugin > Scripts and Styles panel
wp_dequeue_style( 'newsletter-plugin-style' );
wp_deregister_style( 'newsletter-plugin-style' );
}
}, 100 ); // Priority 100 runs after plugins register their assets
// Step 4: Fix cookie banners with fixed positioning to prevent layout shift
/*
CSS to add in Customizer or Additional CSS:
#cookie-notice, .cookie-banner, .cookie-law-info-bar {
position: fixed !important;
bottom: 0 !important;
top: auto !important;
width: 100% !important;
z-index: 9999;
}
*/
// Useful plugin: Query Monitor (free)
// Shows all enqueued scripts and styles with their handles and conditions
Apply CSS contain:layout to dynamic content areas
The CSS contain property tells the browser that a given element is independent from the rest of the document in terms of layout, paint, or style calculations. Setting contain: layout on a sidebar widget area or ad container means that even if content inside that container shifts or loads dynamically, the browser is not required to recalculate the position of elements outside the container. This prevents a single late-loading widget from causing the entire page to reflow. Use contain: strict for maximum isolation when you know the container has a fixed size, or contain: layout paint for containers that may have variable height but should not affect external elements.
/* Add to your theme's style.css or Customizer Additional CSS */
/* Sidebar widget areas: prevent widget reflow from affecting main content */
.widget-area,
.sidebar,
#secondary {
contain: layout;
}
/* Individual widgets with dynamic content */
.widget,
.wp-block-widget-group {
contain: layout;
}
/* Ad containers: full containment for fixed-size slots */
.ad-unit,
.advertisement,
[class*="ad-slot"] {
contain: strict; /* layout + style + size: only when size is known */
min-height: 250px; /* always define minimum height */
}
/* WooCommerce product grids: isolate each card */
.woocommerce ul.products li.product {
contain: layout paint;
}
/* Footer widget areas */
.footer-widgets,
.site-footer .widget-area {
contain: layout;
}
/* Note: Do not use contain:strict on elements with auto-height content.
Use contain:layout when height is not fixed.
Use contain:strict only for fixed-size containers like known ad units. */
Quick checklist
-
All
<img>tags in theme templates usewp_get_attachment_image()or have explicitwidthandheightattributes -
Ad containers and iframe embeds have
aspect-ratioormin-heightset to their expected dimensions -
All
@font-facedeclarations includefont-display: swapand critical fonts have arel="preload"link -
Cookie banners and notification bars use
position: fixedso they do not push page content -
Sidebar widget areas and ad containers have
contain: layoutapplied in theme CSS - Chrome DevTools Layout Shift Regions shows no blue highlights on initial page load (logged-out user)
Frequently asked questions
The most common CLS causes in WordPress are images without width and height attributes that cause reflow as they load, web fonts swapping in after fallback text is rendered, ads and embeds loading without pre-reserved space, plugins injecting banners or cookie notices that push content down, and the WordPress admin bar adding height for logged-in users. Each of these causes elements already visible on screen to move, increasing the CLS score.
Ads are one of the largest CLS contributors across the web. Ad networks inject iframes whose dimensions are unknown until the creative loads, causing surrounding content to jump. Fix this by always defining a minimum height on ad containers via CSS min-height or aspect-ratio matching the expected unit size (90px for a standard leaderboard, 250px for a medium rectangle). Ad Inserter and Advanced Ads both support placeholder dimension settings that reserve space before the creative loads.
Elementor can contribute to CLS through scroll-triggered animations that move elements, font loading without font-display:swap, unsized background images on section elements, and sticky headers that recalculate layout after initial paint. To reduce Elementor CLS, disable entrance animations for above-the-fold sections, add font-display:swap via Elementor's custom CSS panel, and define explicit heights on hero sections rather than relying on auto height with background images.
Open Chrome DevTools, access the Rendering panel via the three-dot menu or Ctrl+Shift+P, and enable Layout Shift Regions. Reload the page and watch for blue rectangles highlighting shifted elements. For more detail, run a Lighthouse audit, which reports the specific elements contributing to CLS with their individual scores. The Performance timeline also marks layout shifts as red triangles you can click to inspect the associated elements.
GeneratePress, Kadence, and Astra consistently achieve the lowest CLS scores. They avoid common pitfalls: all images use explicit dimensions, no JavaScript-driven layout changes above the fold, no auto-loading font swaps, and no sticky headers that recalculate position. The default WordPress themes (Twenty Twenty-Four and later block themes) also perform well since they adhere to block theme standards. Page builder themes like Divi and Avada require more manual configuration to achieve a Good CLS score of 0.1 or below.
Set up real-user monitoring using the web-vitals JavaScript library (1.5KB). Send CLS data to your analytics platform (Google Analytics 4, custom endpoint). The attribution build identifies exactly which element caused each layout shift. For Wordpress, also monitor CLS after route transitions, as client-side navigation can trigger additional shifts not captured in initial page load.
Related resources
Complete CLS Guide
Deep dive into CLS measurement, thresholds, shift sources, and optimization strategies across all platforms and frameworks.
FixFix CLS in Shopify
Shopify-specific CLS fixes including Liquid template image attributes, section schema settings, and app embed management.
FixFix CLS in Next.js
Next.js CLS fixes using next/image, font optimization, and streaming with Suspense boundaries to prevent layout shifts.
Continue learning
Complete CLS Guide
Deep dive into CLS -- thresholds, measurement, and optimization strategies.
FixFix LCP in WordPress
Related performance optimization for the same framework.
FixFix INP in WordPress
Related performance optimization for the same framework.
ToolCWV Score Explainer
Enter your scores for personalized fix recommendations.