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

Fix CLS Issues on Expressionengine (Layout Shift)

Stabilize ExpressionEngine layouts by sizing channel image fields, preloading template fonts, and constraining module tag output.

General Guide: See Global CLS Guide for universal concepts and fixes.

What is CLS?

Cumulative Layout Shift measures visual stability. Google recommends CLS under 0.1. ExpressionEngine generates CLS from unsized image fields in channel entries, dynamic module tag output, and late-loading add-on widgets.

ExpressionEngine-Specific CLS Causes

  • Channel entry images without dimensions -- File fields and Rich Text (Wygwam) fields output <img> tags without width/height
  • Dynamic channel entry listings -- {exp:channel:entries} renders variable numbers of entries at variable heights
  • Add-on widget injection -- third-party add-ons inject content (forms, sliders, accordions) that changes height after initial render
  • AJAX-loaded content -- EE templates using AJAX to load content below-fold inject elements that shift the page
  • Google Fonts in theme CSS -- EE themes load web fonts via @import causing FOUT

Fixes

1. Add Dimensions to Channel Entry Images

In your channel entry templates, always include width and height:

{exp:channel:entries channel="blog" limit="10"}
  <article class="post-card">
    {if blog_thumbnail}
      <div class="post-image" style="aspect-ratio: 16/9; overflow: hidden;">
        <img
          src="{blog_thumbnail}"
          width="400" height="225"
          alt="{title}"
          loading="lazy"
          style="width: 100%; height: 100%; object-fit: cover;"
        >
      </div>
    {if:else}
      <div class="post-image-placeholder" style="aspect-ratio: 16/9; background: #e5e5e5;"></div>
    {/if}
    <h2><a href="{url_title_path='blog/entry'}">{title}</a></h2>
  </article>
{/exp:channel:entries}

For Wygwam (rich text) content that contains inline images:

/* Fix images in Wygwam content output */
.entry-content img {
  max-width: 100%;
  height: auto;
}

.entry-content img:not([width]) {
  aspect-ratio: 16 / 9;
  width: 100%;
  object-fit: cover;
}

2. Stabilize Channel Listing Containers

/* Consistent heights for entry listings */
.post-card {
  min-height: 300px;
  contain: layout;
}

/* Calendar/event listings */
.event-item {
  min-height: 120px;
  contain: layout;
}

/* Search results */
.search-result {
  min-height: 100px;
  contain: layout;
}

3. Handle Add-On Widget Loading

Wrap dynamically-loaded add-on output in reserved containers:

{!-- Form add-on --}
<div class="form-container" style="min-height: 400px; contain: layout;">
  {exp:freeform:form form="contact"}
    {!-- form fields --}
  {/exp:freeform:form}
</div>

{!-- Slider/carousel add-on --}
<div class="slider-container" style="min-height: 500px; contain: layout; aspect-ratio: 16/9;">
  {exp:mx_slider channel="slides" limit="5"}
    {!-- slide content --}
  {/exp:mx_slider}
</div>

4. Preload Fonts

{!-- In your layout template <head> --}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
@font-face {
  font-family: 'Roboto Fallback';
  src: local('Arial');
  size-adjust: 100.4%;
  ascent-override: 92%;
}

body { font-family: 'Roboto', 'Roboto Fallback', Arial, sans-serif; }

5. Handle Embed Content in Entries

/* Responsive embeds in channel entry content */
.entry-content iframe {
  aspect-ratio: 16 / 9;
  width: 100%;
  height: auto;
  border: 0;
}

.entry-content .twitter-tweet {
  min-height: 400px;
  contain: layout;
}

Measuring CLS on ExpressionEngine

  1. Chrome DevTools Performance tab -- look for layout-shift entries during page load
  2. EE Debug Output -- enable to see which templates and tags are rendering (helps correlate shifts with specific tags)
  3. Test pages with variable content -- blog listings with different post counts, entries with/without images
  4. Mobile testing -- EE themes often have responsive layouts with different CLS on mobile

Analytics Script Impact

  • EE analytics are typically in template partials -- ensure they use async/defer
  • Cookie consent add-ons should use fixed-position overlays
  • Heatmap tools injecting overlay elements should be fixed-positioned