Fix Cumulative Layout Shift (CLS) on HubSpot | Blue Frog Docs

Fix Cumulative Layout Shift (CLS) on HubSpot

Reduce layout shifts and improve CLS scores on HubSpot CMS by stabilizing modules, forms, and dynamic content.

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

  1. Go to PageSpeed Insights
  2. Enter your HubSpot URL
  3. View Cumulative Layout Shift metric
  4. Check Diagnostics → "Avoid large layout shifts"
  5. See specific elements causing shifts

Use Chrome DevTools

  1. Open page in Chrome
  2. Press F12 → Performance tab
  3. Enable "Web Vitals" in settings
  4. Record page load
  5. 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:

  • Blog recent posts module
  • CTA module
  • Meeting scheduler embed
  • Live chat widget

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

  1. Images without dimensions - Always set width/height
  2. Dynamic content without placeholder - Reserve space
  3. Injecting content above existing content - Use transform or fixed positions
  4. Animations that change layout - Use transform and opacity
  5. Unsized embeds - Set dimensions for iframes, videos, ads

Quick Wins for HubSpot

Priority fixes for immediate CLS improvement:

  1. Add width/height to all images (HubSpot modules and custom)
  2. Set min-height for HubSpot forms (500px typical)
  3. Use aspect-ratio for responsive images
  4. Reserve space for Smart Content (matching heights)
  5. Fix blog listing thumbnails (set aspect ratio)

Monitoring CLS Over Time

Google Search Console

Continuous Monitoring

  • Set up automated PageSpeed testing
  • Monitor after template changes
  • Test new modules before publishing

Next Steps

For general CLS concepts, see Core Web Vitals Guide.

// SYS.FOOTER