Fix CLS Issues on Joomla (Layout Shift) | OpsBlu Docs

Fix CLS Issues on Joomla (Layout Shift)

Stabilize Joomla layouts by sizing template images, preloading web fonts, and reserving space for module position injection.

Cumulative Layout Shift (CLS) measures visual stability. Joomla sites often suffer from CLS due to dynamic module insertion, web fonts, template layouts, and extension-injected content. This guide provides Joomla-specific solutions.

What is CLS?

CLS is a Core Web Vital that measures unexpected layout shifts during page load.

Thresholds:

  • Good: < 0.1
  • Needs Improvement: 0.1 - 0.25
  • Poor: > 0.25

Common CLS Causes on Joomla:

  • Images without width/height attributes
  • Web fonts loading (FOUT/FOIT)
  • Dynamic module/ad insertion
  • Cookie consent banners
  • Lazy-loaded images
  • Embeds (YouTube, Twitter, Instagram)

Measuring Joomla CLS

Google PageSpeed Insights

  1. Go to PageSpeed Insights
  2. Enter your Joomla URL
  3. View Cumulative Layout Shift metric
  4. Check Diagnostics → Avoid large layout shifts for specific elements

Chrome DevTools

Layout Shift Regions (Visual):

  1. Open DevTools → Performance tab
  2. Check Experience checkbox
  3. Click Record and reload page
  4. Look for red "Layout Shift" markers
  5. Click marker to see which element shifted

Console Logging:

// Log all layout shifts
let cls = 0;
new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
            cls += entry.value;
            console.log('Layout Shift:', entry.value, 'Total CLS:', cls);
            console.log('Shifted elements:', entry.sources);
        }
    }
}).observe({type: 'layout-shift', buffered: true});

Web Vitals Chrome Extension

Install Web Vitals to see CLS on every Joomla page.

Common Joomla CLS Issues

1. Images Without Dimensions

Symptoms:

  • Content jumps down as images load
  • Article images cause shifts
  • VirtueMart/J2Store product images shift layout

Solution: Always Set Width & Height

In Joomla articles:

<!-- Always include width and height -->
<img src="images/article-image.jpg" width="800" height="600" alt="Article Image">

In template:

<?php
// Ensure images have dimensions
$image = JImage::getInstance($imagePath);
$properties = $image->getImageFileProperties($imagePath);
?>
<img src="<?php echo $imagePath; ?>"
     width="<?php echo $properties->width; ?>"
     height="<?php echo $properties->height; ?>"
     alt="Image">

CSS aspect-ratio:

/* In template CSS */
img {
    max-width: 100%;
    height: auto;
}

.article-image {
    aspect-ratio: 16 / 9;
    width: 100%;
}

2. Lazy Loading Images

Symptoms:

  • Images above the fold cause layout shifts
  • Lazy load placeholder → full image causes jump

Solutions:

Don't Lazy Load Above-the-Fold Images

<!-- Hero/featured images: eager loading -->
<img src="hero.jpg" width="1920" height="1080" loading="eager" fetchpriority="high" alt="Hero">

<!-- Below the fold: lazy loading -->
<img src="content.jpg" width="800" height="400" loading="lazy" alt="Content">

Disable lazy loading for specific images in JCH Optimize:

Components → JCH Optimize → Settings
Image Optimization:
- Lazy Load Images: Yes
- Exclude Images: Add hero image classes/IDs

Use Placeholder with Same Dimensions

<!-- SVG placeholder with aspect ratio -->
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 9'%3E%3C/svg%3E"
     data-src="actual-image.jpg"
     width="1600"
     height="900"
     class="lazy"
     alt="Image">

3. Web Fonts Loading (FOUT/FOIT)

Symptoms:

  • Text appears, then changes font (Flash of Unstyled Text)
  • Text invisible, then appears (Flash of Invisible Text)
  • Headers/body text jumps when font loads

Solutions:

Use font-display: swap

@font-face {
    font-family: 'Your Custom Font';
    src: url('/templates/your-template/fonts/font.woff2') format('woff2');
    font-display: swap; /* Show fallback immediately */
}

For Google Fonts:

<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">

Preload Critical Fonts

// In template index.php
<?php
$doc = JFactory::getDocument();
$templatePath = $this->baseurl . '/templates/' . $this->template;
$doc->addHeadLink($templatePath . '/fonts/primary-font.woff2', 'preload', 'rel', [
    'as' => 'font',
    'type' => 'font/woff2',
    'crossorigin' => 'anonymous'
]);
?>

Use System Fonts (No CLS)

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}

4. Dynamic Module/Ad Insertion

Symptoms:

  • Content jumps as modules load
  • Sidebar modules push content down
  • In-content ads shift paragraphs

Solutions:

Reserve Module Space with Min-Height

/* In template CSS */
.moduletable {
    min-height: 250px; /* Match typical module height */
}

/* For specific module positions */
#sidebar .moduletable {
    min-height: 300px;
}

For ads:

.ad-module {
    min-height: 250px;
    min-width: 300px;
    background: #f5f5f5; /* Placeholder */
}

/* Responsive ads */
.ad-module {
    aspect-ratio: 300 / 250;
    width: 100%;
    max-width: 300px;
}

Load Modules Inline (Not Via AJAX)

If modules load via AJAX, they cause CLS. Load them inline instead:

<!-- In template position -->
<jdoc:include type="modules" name="sidebar" style="xhtml" />

Symptoms:

  • Banner pushes content down on first visit
  • Banner appears after page loads

Solutions:

Position Banner as Overlay

/* Don't push content - overlay instead */
.cookie-banner {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 9999;
    /* This doesn't push content, no CLS */
}

Popular Joomla Cookie Extensions:

  • CookieNotice - Configure to overlay
  • iubenda - Set position: fixed
  • Complianz GDPR/CCPA - Use overlay mode

6. Template Layout Changes

Symptoms:

  • Template elements jump/resize on load
  • Menu changes size
  • Headers/footers shift

Solutions:

Define Heights for Template Elements

/* Template CSS */
.site-header {
    min-height: 80px; /* Prevents collapse */
}

.main-navigation {
    height: 60px;
}

.site-footer {
    min-height: 200px;
}

Use CSS Grid/Flexbox with Fixed Dimensions

.site-wrapper {
    display: grid;
    grid-template-rows: 80px 1fr 200px; /* header, content, footer */
    grid-template-columns: 250px 1fr; /* sidebar, main */
    min-height: 100vh;
}

7. Joomla Module Chrome

Symptoms:

  • Modules change style/layout after load
  • Module titles/styling shift

Solutions:

Optimize Module Chrome

// In templates/your-template/html/modules.php
<?php
defined('_JEXEC') or die;

function modChrome_optimized($module, &$params, &$attribs)
{
    // Consistent module structure with fixed dimensions
    $moduleClass = htmlspecialchars($params->get('moduleclass_sfx'));
    ?>
    <div class="moduletable <?php echo $moduleClass; ?>" style="min-height: 100px;">
        <?php if ($module->showtitle) : ?>
            <h3 class="module-title"><?php echo $module->title; ?></h3>
        <?php endif; ?>
        <div class="module-content">
            <?php echo $module->content; ?>
        </div>
    </div>
    <?php
}

8. Embeds (YouTube, Social Media)

Symptoms:

  • Embedded videos cause layout shift
  • Social media embeds (Twitter, Facebook) jump

Solutions:

Reserve Space with Aspect Ratio

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

CSS solution:

.video-embed {
    position: relative;
    aspect-ratio: 16 / 9;
    width: 100%;
}

.video-embed iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

Use Lazy Load for Embeds

<!-- Load iframe on click -->
<div class="video-placeholder" data-video-id="VIDEO_ID" style="aspect-ratio: 16/9; background: url('thumbnail.jpg');">
    <button Video</button>
</div>

<script>
function loadVideo(button) {
    const container = button.parentElement;
    const videoId = container.getAttribute('data-video-id');
    container.innerHTML = `<iframe src="https://www.youtube.com/embed/${videoId}?autoplay=1" style="width:100%;height:100%;" allowfullscreen></iframe>`;
}
</script>

9. VirtueMart/J2Store/HikaShop Product Pages

Symptoms:

  • Product images shift layout
  • Price/availability info jumps
  • Add to cart button moves

Solutions:

Reserve Product Image Space

/* VirtueMart */
.product-container .vm-product-media-container {
    aspect-ratio: 1 / 1; /* Square product images */
    width: 100%;
}

/* J2Store */
.j2store-product-images {
    min-height: 400px;
    aspect-ratio: 1 / 1;
}

/* HikaShop */
.hikashop_product_image {
    aspect-ratio: 1 / 1;
    width: 100%;
}

Set Fixed Heights for Product Info

.product-price {
    min-height: 30px;
}

.product-availability {
    min-height: 20px;
}

.addtocart-bar {
    min-height: 50px;
}

Advanced Joomla CLS Fixes

CSS Containment

/* Limit layout impact of dynamic content */
.module-container {
    contain: layout style;
}

.article-content {
    contain: layout;
}

Transform Instead of Layout Properties

/* Use transform for animations (no CLS) */
.dropdown-menu {
    transform: translateY(-100%);
    transition: transform 0.3s;
}

.dropdown-menu.open {
    transform: translateY(0);
}

/* Don't use top/height (causes CLS) */
.dropdown-menu-bad {
    top: -100px; /* Causes layout shift */
    transition: top 0.3s;
}

Preload Key Resources

// In template index.php
<?php
$doc = JFactory::getDocument();

// Preload critical CSS
$doc->addHeadLink($this->baseurl . '/templates/' . $this->template . '/css/template.css', 'preload', 'rel', ['as' => 'style']);

// Preload fonts
$doc->addHeadLink($this->baseurl . '/templates/' . $this->template . '/fonts/main.woff2', 'preload', 'rel', [
    'as' => 'font',
    'type' => 'font/woff2',
    'crossorigin' => 'anonymous'
]);
?>

Testing CLS Improvements

Before/After Comparison

  1. Baseline Measurement:

    • Run PageSpeed Insights
    • Record CLS score
    • Note specific shifting elements
  2. Make Changes

  3. Re-test:

    • Clear all caches
    • Run PageSpeed Insights
    • Compare CLS improvement

Layout Shift GIF

Use Web Vitals extension to create visual layout shift GIF:

1. Install extension
2. Open your Joomla site
3. Click extension icon
4. View Layout Shifts
5. Generate GIF of shifts

Real User Monitoring

// Track CLS in production
let clsValue = 0;
new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
            clsValue += entry.value;
        }
    }

    // Send to analytics
    if (typeof gtag !== 'undefined') {
        gtag('event', 'cls_measured', {
            'value': clsValue,
            'page': window.location.pathname
        });
    }
}).observe({type: 'layout-shift', buffered: true});

Joomla CLS Checklist

  • All images have width and height attributes
  • Don't lazy load above-the-fold images
  • Use font-display: swap for web fonts
  • Preload critical fonts
  • Reserve space for ads/modules with min-height
  • Position cookie banners as fixed overlay
  • Set aspect ratio for embeds (YouTube, etc.)
  • Define template element heights (header, footer)
  • Optimize module chrome
  • Use CSS containment for dynamic content
  • Test with JCH Optimize image optimization
  • Check product pages (VirtueMart/J2Store/HikaShop)

Next Steps