Fix Cumulative Layout Shift (CLS) on HubSpot
Cumulative Layout Shift (CLS) measures visual stability during page load. High CLS creates a poor user experience when content unexpectedly moves. This guide covers HubSpot-specific CLS fixes.
What is CLS?
CLS measures: Unexpected layout shifts during page load
Target scores:
- Good: < 0.1
- Needs Improvement: 0.1 - 0.25
- Poor: > 0.25
Common causes on HubSpot:
- Images without dimensions
- HubSpot forms loading
- Smart Content variations
- Dynamic modules
- Ads and embeds
Measuring CLS on HubSpot
Use PageSpeed Insights
- Go to PageSpeed Insights
- Enter your HubSpot URL
- View Cumulative Layout Shift metric
- Check Diagnostics → "Avoid large layout shifts"
- See specific elements causing shifts
Use Chrome DevTools
- Open page in Chrome
- Press F12 → Performance tab
- Enable "Web Vitals" in settings
- Record page load
- Look for red "Layout Shift" bars
Visual Debugging
// Add to console to visualize shifts
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) return;
console.log('Layout shift:', entry.value, entry.sources);
}
}).observe({type: 'layout-shift', buffered: true});
Common CLS Issues on HubSpot
1. Images Without Dimensions
Problem: Images load and push content down
Impact: Largest CLS contributor on most sites
Solution: Add Width & Height
In HubSpot image modules:
{% module "image"
path="@hubspot/image",
img={
"src": "your-image.jpg",
"alt": "Description",
"width": 800, {# Actual image width #}
"height": 600 {# Actual image height #}
}
%}
In custom HTML/HubL:
<img src="image.jpg"
alt="Description"
width="800"
height="600">
For responsive images:
img {
width: 100%;
height: auto;
/* Browser reserves space based on width/height ratio */
}
Calculate aspect ratio:
.image-container {
/* For 16:9 image */
aspect-ratio: 16 / 9;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
2. HubSpot Forms Loading
Problem: Forms load and shift page layout
Symptoms:
- Content jumps when form appears
- Buttons move after form loads
- CLS spike on form-heavy pages
Solution: Reserve Space for Form
Method 1: Set Min-Height
/* Add to stylesheet or page CSS */
.hbspt-form {
min-height: 500px; /* Approximate form height */
}
Method 2: Use Aspect Ratio Box
<div class="form-container">
<!-- HubSpot form embed code here -->
</div>
<style>
.form-container {
position: relative;
padding-bottom: 80%; /* Adjust based on form height */
height: 0;
}
.form-container .hbspt-form {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
Method 3: Hidden Placeholder
<div class="form-wrapper">
<!-- Placeholder while form loads -->
<div class="form-placeholder" style="height: 500px;">
Loading form...
</div>
<!-- Actual form -->
<div class="hbspt-form"></div>
</div>
<script>
// Hide placeholder when form ready
window.addEventListener('message', function(event) {
if (event.data.type === 'hsFormCallback' && event.data.eventName === 'onFormReady') {
document.querySelector('.form-placeholder').style.display = 'none';
}
});
</script>
3. Smart Content Variations
Problem: Smart Content causes layout shift when switching variants
Symptoms:
- Content changes size between variants
- Different variants have different heights
- Personalized content shifts page
Solution: Fixed Heights for Variants
{% smart_content "pricing_section" %}
{% smart_rule "customer" %}
<div class="smart-content-box" style="min-height: 300px;">
<h2>Customer Content</h2>
<p>Special pricing for existing customers.</p>
</div>
{% end_smart_rule %}
{% smart_rule "lead" %}
<div class="smart-content-box" style="min-height: 300px;">
<h2>Lead Content</h2>
<p>Sign up to see pricing.</p>
</div>
{% end_smart_rule %}
{% end_smart_content %}
<style>
.smart-content-box {
min-height: 300px; /* Same for all variants */
}
</style>
Alternative: Server-Side Only
Ensure Smart Content renders server-side (default) rather than client-side to avoid visible shifts.
4. Dynamic HubSpot Modules
Problem: Modules that load content dynamically
Common culprits:
Solution: Reserve Space
{% module "recent_posts"
path="@hubspot/blog_recent_posts"
%}
<style>
/* Reserve space for module */
.widget-type-blog_recent_posts {
min-height: 400px;
}
</style>
5. Fonts Loading (FOUT/FOIT)
Problem: Web fonts load and change text size/layout
FOUT: Flash of Unstyled Text FOIT: Flash of Invisible Text
Solution: Use font-display
@font-face {
font-family: 'YourFont';
src: url('your-font.woff2') format('woff2');
font-display: swap; /* Show fallback immediately */
}
body {
font-family: 'YourFont', Arial, sans-serif;
/* Fallback font with similar metrics */
}
Match fallback font metrics:
@font-face {
font-family: 'YourFont';
src: url('your-font.woff2') format('woff2');
font-display: swap;
size-adjust: 105%; /* Adjust to match primary font */
}
6. Ads and Third-Party Embeds
Problem: Ad slots or embeds shift content when loading
Solution: Fixed Containers
<!-- Ad slot with fixed size -->
<div class="ad-slot" style="width: 300px; height: 250px;">
<!-- Ad code here -->
</div>
<!-- YouTube embed with aspect ratio -->
<div class="video-container">
<iframe src="https://www.youtube.com/embed/VIDEO_ID"></iframe>
</div>
<style>
.video-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
height: 0;
overflow: hidden;
}
.video-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
HubSpot-Specific Fixes
1. Blog Listing Pages
Problem: Blog post thumbnails load and shift layout
Solution:
{% for content in contents %}
<article class="blog-post">
<a href="{{ content.absolute_url }}">
{% if content.featured_image %}
<div class="post-image" style="aspect-ratio: 16/9;">
<img src="{{ content.featured_image }}"
alt="{{ content.featured_image_alt_text }}"
width="800"
height="450"
loading="lazy">
</div>
{% endif %}
<h2>{{ content.name }}</h2>
</a>
</article>
{% endfor %}
<style>
.post-image {
overflow: hidden;
background: #f0f0f0; /* Placeholder color */
}
.post-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
2. HubSpot CTAs
Problem: CTA modules load and shift content
Solution:
{% module "cta"
path="@hubspot/cta"
%}
<style>
/* Reserve space for CTA */
.hs-cta-wrapper {
min-height: 60px;
display: flex;
align-items: center;
}
</style>
3. Personalization Tokens
Problem: Personalization tokens change text length
{# May cause shift if name is long/short #}
<h1>Welcome, {{ contact.firstname }}!</h1>
{# Better: Fixed width container #}
<h1 style="min-width: 200px;">Welcome, {{ contact.firstname|truncate(20) }}!</h1>
4. Meeting Scheduler Widget
Problem: Meeting widget loads and expands page
Solution:
<div class="meeting-widget-container" style="min-height: 600px;">
<!-- HubSpot meeting embed code -->
</div>
5. Live Chat Widget
Problem: Chat widget pops in and shifts footer
Solution:
HubSpot chat loads at bottom-right by default (usually doesn't cause CLS). If customized:
/* Ensure chat doesn't shift content */
#hubspot-messages-iframe-container {
position: fixed !important;
bottom: 0 !important;
right: 0 !important;
}
Advanced Techniques
CSS Containment
Tell browser to isolate module layout:
.module-container {
contain: layout size;
/* Module changes don't affect parent layout */
}
Reserve Space with Skeleton Screens
<div class="content-skeleton">
<div class="skeleton-image" style="height: 400px; background: #f0f0f0;"></div>
<div class="skeleton-text" style="height: 20px; background: #f0f0f0; margin: 10px 0;"></div>
<div class="skeleton-text" style="height: 20px; background: #f0f0f0;"></div>
</div>
<!-- Replace with actual content when loaded -->
Transform Instead of Layout Properties
/* Bad: Causes layout shift */
.element {
margin-top: 20px;
}
/* Good: No layout shift */
.element {
transform: translateY(20px);
}
Testing CLS Improvements
Field Data vs. Lab Data
- Lab data: PageSpeed Insights, Lighthouse
- Field data: Real user experience (Search Console)
Test both!
Test User Interactions
CLS can occur after initial load:
- Form submissions
- Button clicks
- Tab switches
- Accordion expansions
Test on Slow Connections
Chrome DevTools:
Network tab → Throttling → Slow 3G
This reveals layout shifts that happen quickly on fast connections.
Common Mistakes to Avoid
- ❌ Images without dimensions - Always set width/height
- ❌ Dynamic content without placeholder - Reserve space
- ❌ Injecting content above existing content - Use transform or fixed positions
- ❌ Animations that change layout - Use transform and opacity
- ❌ Unsized embeds - Set dimensions for iframes, videos, ads
Quick Wins for HubSpot
Priority fixes for immediate CLS improvement:
- ✅ Add width/height to all images (HubSpot modules and custom)
- ✅ Set min-height for HubSpot forms (500px typical)
- ✅ Use aspect-ratio for responsive images
- ✅ Reserve space for Smart Content (matching heights)
- ✅ Fix blog listing thumbnails (set aspect ratio)
Monitoring CLS Over Time
Google Search Console
- Core Web Vitals report
- Shows real-user CLS
- Groups URLs by status (Good/Needs Improvement/Poor)
Continuous Monitoring
- Set up automated PageSpeed testing
- Monitor after template changes
- Test new modules before publishing
Next Steps
- Fix LCP Issues - Improve load speed
- Optimize Images - HubSpot best practices
- Test Performance - Full performance guide
For general CLS concepts, see Core Web Vitals Guide.