Fix CLS Issues on Cs Cart (Layout Shift) | OpsBlu Docs

Fix CLS Issues on Cs Cart (Layout Shift)

Stabilize CS-Cart layouts by sizing product thumbnails in Smarty templates, preloading storefront fonts, and reserving add-on containers.

Cumulative Layout Shift (CLS) measures visual stability by tracking unexpected layout shifts during page load. This guide shows you how to fix CLS issues in your CS-Cart store.

What is CLS?

CLS quantifies how much visible content shifts unexpectedly during the page lifecycle. Common causes include:

  • Images without dimensions
  • Ads, embeds, or iframes without dimensions
  • Dynamically injected content
  • Web fonts causing FOIT/FOUT
  • Actions waiting for network response

CLS Scoring

  • Good: 0-0.1 (green)
  • Needs Improvement: 0.1-0.25 (orange)
  • Poor: Over 0.25 (red)

Goal: Achieve CLS under 0.1 for 75% of page views.

Diagnosing CLS Issues

Use PageSpeed Insights

  1. Go to PageSpeed Insights
  2. Enter your CS-Cart store URL
  3. Check Cumulative Layout Shift metric
  4. Expand Diagnostics to see "Avoid large layout shifts"
  5. Review elements causing shifts

Use Chrome DevTools

Experience Panel:

  1. Open DevTools (F12)
  2. Press Ctrl+Shift+P (Cmd+Shift+P on Mac)
  3. Type "Show Rendering"
  4. Enable Layout Shift Regions
  5. Reload page and watch for blue highlights

Performance Panel:

  1. Open Performance tab
  2. Click Record
  3. Reload page
  4. Stop recording
  5. Look for Layout Shift events (red bars)
  6. Click on shifts to see details

Measure CLS with JavaScript

let clsValue = 0;
let clsEntries = [];

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value;
      clsEntries.push(entry);
      console.log('Layout shift:', entry.value, entry.sources);
    }
  }
  console.log('Current CLS:', clsValue);
});

observer.observe({type: 'layout-shift', buffered: true});

Common CLS Issues in CS-Cart

1. Images Without Dimensions

Problem: Images load and push content down.

Solution: Always specify width and height attributes.

Product Images:

File: design/themes/[theme]/templates/blocks/product_list_templates/grid.tpl

{* Before - causes layout shift *}
<img src="{$product.main_pair.icon.image_path}"
     alt="{$product.product|escape}">

{* After - reserves space *}
<img src="{$product.main_pair.icon.image_path}"
     alt="{$product.product|escape}"
     width="{$product.main_pair.icon.image_x}"
     height="{$product.main_pair.icon.image_y}">

Responsive Images with Aspect Ratio:

{* Calculate aspect ratio *}
{assign var="aspect_ratio" value=$product.main_pair.icon.image_y/$product.main_pair.icon.image_x*100}

<div style="position: relative; width: 100%; padding-bottom: {$aspect_ratio}%;">
  <img src="{$product.main_pair.icon.image_path}"
       alt="{$product.product|escape}"
       style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
</div>

Using CSS aspect-ratio:

.product-image {
  aspect-ratio: 1 / 1; /* For square images */
  width: 100%;
  height: auto;
}
<img src="{$product.main_pair.icon.image_path}"
     alt="{$product.product|escape}"
     class="product-image"
     width="300"
     height="300">

2. Dynamic Content Loading

Problem: Content loads after page render, pushing existing content.

Banner Blocks:

{* Reserve space for banner *}
<div class="banner-container" style="min-height: 400px;">
  {include file="blocks/banner.tpl"}
</div>

Product Blocks:

/* Reserve space before products load */
.ty-grid-list {
  min-height: 600px;
}

.ty-grid-list__item {
  min-height: 350px;
}

AJAX-Loaded Content:

// Before loading content, set container height
var container = document.getElementById('ajax-container');
container.style.minHeight = '500px';

// Load content
$.ajax({
  url: 'content.html',
  success: function(data) {
    container.innerHTML = data;
    container.style.minHeight = ''; // Remove after loaded
  }
});

3. Web Fonts Causing FOUT/FOIT

Problem: Text shifts when web fonts load.

Solution 1: Use font-display: swap

@font-face {
  font-family: 'YourFont';
  src: url('/fonts/your-font.woff2') format('woff2');
  font-display: swap; /* Show fallback immediately, swap when loaded */
}

Solution 2: Preload Fonts

<link rel="preload"
      href="/fonts/your-font.woff2"
      as="font"
      type="font/woff2"
      crossorigin>

Solution 3: Font Loading API

// Load font before showing text
if ('fonts' in document) {
  Promise.all([
    document.fonts.load('1rem YourFont'),
    document.fonts.load('bold 1rem YourFont')
  ]).then(() => {
    document.body.classList.add('fonts-loaded');
  });
}
/* Match fallback font metrics */
body {
  font-family: Arial, sans-serif;
}

body.fonts-loaded {
  font-family: 'YourFont', Arial, sans-serif;
}

4. Ads and Embeds

Problem: Ad slots without dimensions cause shifts.

Google AdSense:

<!-- Reserve exact space -->
<div style="width: 728px; height: 90px;">
  <!-- Ad code here -->
</div>

Responsive Ads:

/* Use aspect ratio boxes */
.ad-container {
  position: relative;
  width: 100%;
  padding-bottom: 12.37%; /* 90/728 for 728x90 banner */
}

.ad-container > * {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

YouTube/Video Embeds:

<!-- Maintain 16:9 aspect ratio -->
<div style="position: relative; padding-bottom: 56.25%; height: 0;">
  <iframe src="https://www.youtube.com/embed/..."
          style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
          frameborder="0"
          allowfullscreen></iframe>
</div>

5. Notification Banners

Problem: Cookie consent or promotional banners pushing content.

Reserve Space at Top:

/* Reserve space for cookie banner */
body.has-cookie-banner {
  padding-top: 60px;
}

.cookie-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 60px;
  z-index: 9999;
}

Or Use Overlay Instead of Pushing:

.notification-banner {
  position: fixed;
  bottom: 0; /* Don't push content */
  left: 0;
  right: 0;
  z-index: 9999;
}

6. CS-Cart Product Options

Problem: Product options loading and expanding content.

File: design/themes/[theme]/templates/views/products/components/product_options.tpl

/* Reserve space for product options */
.ty-product-options {
  min-height: 150px; /* Adjust based on typical option height */
}

Skeleton Loading:

/* Show skeleton while options load */
.ty-product-options.loading {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
}

@keyframes loading {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

7. Lazy-Loaded Images

Problem: Images appearing causes content shift.

Solution: Placeholder with Same Dimensions

{* Placeholder approach *}
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3C/svg%3E"
     data-src="{$product.main_pair.icon.image_path}"
     alt="{$product.product|escape}"
     width="300"
     height="300"
     class="lazy">

Using Intersection Observer:

const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.classList.remove('lazy');
      imageObserver.unobserve(img);
    }
  });
});

document.querySelectorAll('img.lazy').forEach(img => {
  imageObserver.observe(img);
});

8. CS-Cart Filters and Sidebar

Problem: Sidebar loading causes content shift.

/* Reserve sidebar space */
.ty-mainbox-container {
  display: grid;
  grid-template-columns: 250px 1fr; /* Fixed sidebar width */
}

@media (max-width: 768px) {
  .ty-mainbox-container {
    grid-template-columns: 1fr; /* Stack on mobile */
  }
}

9. Price Updates

Problem: Prices loading or updating via AJAX.

/* Reserve space for price */
.ty-price {
  min-height: 30px;
  display: block;
}

.ty-price-num {
  display: inline-block;
  min-width: 60px; /* Prevent width changes */
}

10. Star Ratings

Problem: Rating stars loading causes shift.

/* Reserve space for ratings */
.ty-product-review-stars {
  height: 20px;
  display: block;
}

CS-Cart-Specific Fixes

Fix Product Grid Layout

File: design/themes/[theme]/css/tygh/grid.less

// Fixed grid item height
.ty-grid-list__item {
  min-height: 400px; // Adjust to your needs

  .ty-grid-list__image {
    height: 250px;
    display: flex;
    align-items: center;
    justify-content: center;

    img {
      max-width: 100%;
      max-height: 100%;
      width: auto;
      height: auto;
    }
  }

  .ty-grid-list__item-name {
    min-height: 40px; // Reserve space for product name
  }
}

Fix Category Banner Shifts

{* File: design/themes/[theme]/templates/categories/view.tpl *}
{if $category_data.main_pair.detailed.image_path}
<div class="category-banner" style="aspect-ratio: 16/9; max-height: 400px;">
  <img src="{$category_data.main_pair.detailed.image_path}"
       alt="{$category_data.category|escape}"
       width="1600"
       height="900"
       style="width: 100%; height: 100%; object-fit: cover;">
</div>
{/if}

Fix Header Logo Shift

/* Reserve exact logo space */
.ty-logo {
  width: 200px; /* Your logo width */
  height: 60px; /* Your logo height */
}

.ty-logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

Fix Add to Cart Button

/* Prevent button size changes */
.ty-btn-add-to-cart {
  min-width: 150px;
  height: 40px;
  padding: 0 20px;
}

/* Loading state */
.ty-btn-add-to-cart.loading {
  /* Same dimensions during loading */
}

Advanced Techniques

Use transform Instead of top/left

Bad (causes layout shift):

.element {
  position: relative;
  top: 20px; /* Causes layout shift */
}

Good (doesn't cause layout shift):

.element {
  transform: translateY(20px); /* No layout shift */
}

Reserve Space with CSS Grid

.product-page {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto auto auto;
  gap: 20px;
}

.product-images {
  grid-column: 1;
  grid-row: 1 / 3;
  min-height: 600px; /* Reserve space */
}

.product-info {
  grid-column: 2;
  grid-row: 1;
}

Skeleton Screens

Create placeholder content that matches loaded content dimensions:

<!-- Skeleton for product block -->
<div class="product-skeleton">
  <div class="skeleton-image" style="height: 250px; background: #f0f0f0;"></div>
  <div class="skeleton-title" style="height: 20px; background: #f0f0f0; margin-top: 10px;"></div>
  <div class="skeleton-price" style="height: 24px; width: 60%; background: #f0f0f0; margin-top: 10px;"></div>
</div>

Animations Without Layout Shift

/* Fade in without shifting */
@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.product-item {
  animation: fadeIn 0.3s;
  /* Don't animate height, width, margin, or padding */
}

Testing and Monitoring

Test in Real Devices

CLS often varies by:

  • Device (mobile vs. desktop)
  • Connection speed
  • Browser

Test on:

  • Mobile devices (actual devices, not just emulation)
  • Slow 3G connections
  • Different browsers (Chrome, Safari, Firefox)

Continuous Monitoring

Google Analytics 4 Web Vitals:

import {getCLS} from 'https://unpkg.com/web-vitals@3/dist/web-vitals.js?module';

getCLS((metric) => {
  gtag('event', 'web_vitals', {
    event_category: 'Web Vitals',
    event_label: metric.id,
    value: Math.round(metric.value * 1000),
    metric_name: 'CLS',
    non_interaction: true
  });
});

Chrome User Experience Report

Check your site's field data:

  1. CrUX Dashboard
  2. Enter your domain
  3. View CLS scores from real users

Quick Fixes Checklist

  • Add width and height to all images
  • Use CSS aspect-ratio for responsive images
  • Set min-height on dynamic content containers
  • Use font-display: swap for web fonts
  • Preload critical fonts
  • Reserve space for ads and embeds
  • Fix notification banner behavior
  • Set fixed dimensions for product grid items
  • Reserve space for product options
  • Use transform instead of top/left for animations
  • Implement skeleton loading for AJAX content
  • Test on mobile devices
  • Monitor CLS with RUM

Common Mistakes to Avoid

1. Don't Animate Layout Properties

Bad:

.button:hover {
  width: 200px; /* Causes layout shift */
  height: 50px; /* Causes layout shift */
}

Good:

.button {
  width: 200px;
  height: 50px;
}

.button:hover {
  transform: scale(1.05); /* No layout shift */
}

2. Don't Insert Content Above Existing Content

Bad:

// Inserting banner at top pushes everything down
document.body.insertBefore(banner, document.body.firstChild);

Good:

// Reserve space from the beginning
// Or use fixed positioning

3. Don't Load Critical Resources Too Late

Bad:

<!-- Critical font loaded late -->
<link rel="stylesheet" href="fonts.css">

Good:

<!-- Preload critical font -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

Troubleshooting

CLS Still High After Fixes

Check:

  1. Test on actual mobile device (not just emulation)
  2. Test with slow network (3G throttling)
  3. Disable browser cache
  4. Check third-party scripts (ads, analytics)
  5. Review AJAX-loaded content

Use Layout Shift Recorder:

// Record all layout shifts
const shifts = [];
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      shifts.push({
        value: entry.value,
        sources: entry.sources.map(s => s.node),
        time: entry.startTime
      });
    }
  }
}).observe({type: 'layout-shift', buffered: true});

// After page load, inspect shifts
console.table(shifts);

Different CLS Scores

If PageSpeed Insights shows different scores than field data:

  • Lab data (PageSpeed) uses simulated environment
  • Field data (CrUX) shows real user experience
  • Focus on improving field data

Layout Shifts Only on Mobile

Common causes:

  • Responsive images without proper sizing
  • Mobile-specific ads or banners
  • Different font rendering on mobile
  • Touch-specific interactions

Additional Resources