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

Fix CLS Issues on Processwire (Layout Shift)

Stabilize ProcessWire layouts by sizing image fields with width/height API methods, preloading template fonts, and constraining Repeater 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. ProcessWire generates CLS from image fields rendered without dimensions, Repeater/RepeaterMatrix fields with variable-height items, dynamically loaded content via AJAX, and web font loading in custom themes.

ProcessWire-Specific CLS Causes

  • Image fields without size calls -- using $image->url directly outputs no width/height; the browser cannot reserve space until the image loads
  • Repeater field variability -- RepeaterMatrix items render different field types at different heights, causing layout recalculations
  • HTMX/AJAX partial loading -- ProcessWire sites using htmx or $page->render() for partial content injection shift surrounding elements
  • CKEditor embed rendering -- embedded videos and iframes in CKEditor content fields have no preset dimensions
  • Google Fonts in themes -- custom themes loading Google Fonts via CSS @import cause FOUT

Fixes

1. Always Use Size Variations for Images

ProcessWire's image API includes width and height on resized variations:

// In your template file
$hero = $page->hero_image;
if ($hero) {
    $img = $hero->size(1200, 630);
    echo "<div class='hero-wrapper' style='aspect-ratio: {$img->width}/{$img->height}; overflow: hidden;'>
        <img src='{$img->url}'
             width='{$img->width}' height='{$img->height}'
             alt='{$page->title}'
             style='width: 100%; height: 100%; object-fit: cover;'>
    </div>";
}

For content images in body fields:

/* Fix unsized images from CKEditor content */
.body-content img {
  max-width: 100%;
  height: auto;
}

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

2. Constrain Repeater and RepeaterMatrix Output

// When rendering RepeaterMatrix items, wrap in fixed containers
foreach ($page->content_blocks as $block) {
    $type = $block->type;
    echo "<div class='block block-{$type}' style='contain: layout;'>";
    echo $block->render(); // Renders the matrix type's template
    echo "</div>";
}
/* Set minimum heights per block type */
.block-text { min-height: 100px; contain: layout; }
.block-image { min-height: 300px; contain: layout; }
.block-gallery { min-height: 400px; contain: layout; }
.block-video { min-height: 350px; aspect-ratio: 16/9; contain: layout; }
.block-cta { min-height: 150px; contain: layout; }

3. Fix CKEditor Embeds

/* Responsive iframe containment for CKEditor content */
.body-content iframe {
  aspect-ratio: 16 / 9;
  width: 100%;
  height: auto;
  border: 0;
}

/* Wrap embeds added via CKEditor */
.body-content .embed-wrapper {
  position: relative;
  aspect-ratio: 16 / 9;
}

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

4. Handle AJAX/HTMX Content Loading

/* Reserve space for AJAX-loaded sections */
[hx-get], [data-ajax-target] {
  min-height: 200px;
  contain: layout;
}

/* Sidebar widgets loaded via AJAX */
.sidebar-ajax {
  min-height: 300px;
  contain: layout;
}

5. Preload Theme Fonts

<!-- In _main.php <head> -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" href="<?= $config->urls->templates ?>fonts/custom.woff2"
      as="font" type="font/woff2" crossorigin>
@font-face {
  font-family: 'CustomFont';
  src: url('fonts/custom.woff2') format('woff2');
  font-display: swap;
  size-adjust: 103%;
}

body { font-family: 'CustomFont', system-ui, sans-serif; }

Measuring CLS on ProcessWire

  1. Chrome DevTools Performance tab -- record page load and filter for layout-shift entries
  2. Test RepeaterMatrix pages -- pages with many matrix block types have the highest CLS risk
  3. Test with AJAX disabled -- if using htmx or custom AJAX, test with it disabled to isolate CLS sources
  4. Mobile testing -- ProcessWire themes are often custom-built and may not handle mobile reflow well

Analytics Script Impact

  • ProcessWire has no built-in analytics -- all tracking is added via template files
  • Ensure analytics scripts use async/defer and do not inject visible DOM elements
  • Heatmap tools that inject feedback buttons should use position: fixed
  • Cookie consent modules (GDPR) should overlay content, not push it down