Fix CLS Issues on Concrete CMS (Layout Shift) | OpsBlu Docs

Fix CLS Issues on Concrete CMS (Layout Shift)

Stabilize Concrete CMS layouts by reserving block container heights, sizing image thumbnails, and preloading stack-loaded web fonts.

Cumulative Layout Shift (CLS) measures visual stability during page load. High CLS frustrates users and negatively impacts SEO rankings.

Target: CLS under 0.1 Good: Under 0.1 | Needs Improvement: 0.1-0.25 | Poor: Over 0.25

For general CLS concepts, see the global CLS guide.

Concrete CMS-Specific CLS Issues

1. Images Without Dimensions

The most common CLS issue is images loading without explicit width/height attributes.

Problem: Images in blocks don't have dimensions set, causing layout shift when they load.

Diagnosis:

  • Run PageSpeed Insights
  • Look for "Image elements do not have explicit width and height"
  • Check for layout shifts in Chrome DevTools Performance tab

Solutions:

A. Set Dimensions in Image Blocks

Always include width and height attributes:

<?php
// In image block template
$image = $this->controller->get('image');

if ($image) {
    $thumbnail = $image->getThumbnail('large');
    ?>
    <img
        src="<?php echo $thumbnail->src; ?>"
        width="<?php echo $thumbnail->width; ?>"
        height="<?php echo $thumbnail->height; ?>"
        alt="<?php echo $image->getTitle(); ?>"
    >
    <?php
}
?>

B. Use Aspect Ratio Boxes

For responsive images, maintain aspect ratio:

/* In your theme CSS */
.image-container {
    position: relative;
    width: 100%;
    padding-bottom: 56.25%; /* 16:9 aspect ratio */
    overflow: hidden;
}

.image-container img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
<!-- In template -->
<div class="image-container">
    <img
        src="<?php echo $thumbnail->src; ?>"
        alt="<?php echo $image->getTitle(); ?>"
    >
</div>

C. CSS Aspect-Ratio Property

Modern browsers support aspect-ratio:

.responsive-image {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
}
<img
    src="<?php echo $thumbnail->src; ?>"
    class="responsive-image"
    alt="<?php echo $image->getTitle(); ?>"
>

2. Dynamic Block Content

Blocks that load content dynamically can cause layout shifts.

Problem: AJAX-loaded content, tabs, accordions, or collapsible sections shifting layout.

Common Culprits:

  • FAQ blocks with expanding answers
  • Tabbed content blocks
  • AJAX-loaded page lists
  • Social media feed blocks
  • Related content blocks

Solutions:

A. Reserve Space for Dynamic Content

/* Reserve minimum height for dynamic content */
.dynamic-content-block {
    min-height: 300px; /* Adjust based on expected content */
}

/* For AJAX loading blocks */
.ajax-content-wrapper {
    min-height: 400px;
    position: relative;
}

.ajax-content-wrapper.loading::before {
    content: '';
    display: block;
    height: 400px;
}

B. Load Content Before Page Render

Instead of AJAX loading after page load, server-side render the content:

<?php
// In custom block controller
public function view()
{
    // Load related pages server-side
    $pageList = new \Concrete\Core\Page\PageList();
    $pageList->filterByPageTypeHandle('blog_entry');
    $pageList->setItemsPerPage(3);
    $pages = $pageList->getResults();

    $this->set('relatedPages', $pages);
}
?>
<!-- In block view template -->
<div class="related-posts">
    <?php foreach ($relatedPages as $page) { ?>
        <div class="post-item">
            <?php echo $page->getCollectionName(); ?>
        </div>
    <?php } ?>
</div>

C. Use CSS Transitions Instead of JavaScript

For accordions and collapsible content:

/* Smooth transitions without layout shift */
.accordion-content {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.3s ease;
}

.accordion.active .accordion-content {
    max-height: 500px; /* Set to max expected height */
}

3. Web Fonts Causing Shifts

Custom fonts loading can cause text to shift (FOIT/FOUT).

Problem: Text initially renders in fallback font, then shifts when custom font loads.

Solutions:

A. Use font-display: swap

@font-face {
    font-family: 'CustomFont';
    src: url('fonts/custom-font.woff2') format('woff2');
    font-display: swap; /* Prevent invisible text */
    font-weight: 400;
    font-style: normal;
}

B. Preload Critical Fonts

<!-- In page template <head> -->
<link
    rel="preload"
    href="<?php echo $this->getThemePath(); ?>/fonts/custom-font.woff2"
    as="font"
    type="font/woff2"
    crossorigin
>

C. Match Fallback Font Metrics

Reduce shift by matching fallback font size:

body {
    font-family: 'CustomFont', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
    /* Adjust fallback to match custom font metrics */
    font-size: 16px;
    line-height: 1.5;
}

/* While font is loading */
.fonts-loading body {
    font-size: 16px; /* Match custom font size */
    letter-spacing: 0; /* Adjust if needed */
}

4. Ads and Third-Party Embeds

Third-party content can cause unexpected layout shifts.

Common Issues:

  • Ad slots without reserved space
  • Social media embeds (Twitter, Instagram, Facebook)
  • YouTube videos without aspect ratio containers
  • Comment systems (Disqus, etc.)

Solutions:

A. Reserve Space for Ad Slots

/* Reserve exact space for ad */
.ad-slot {
    width: 300px;
    height: 250px;
    background: #f0f0f0; /* Placeholder while loading */
}
<div class="ad-slot">
    <!-- Ad code here -->
</div>

B. Video Embeds with Aspect Ratio

.video-container {
    position: relative;
    padding-bottom: 56.25%; /* 16:9 */
    height: 0;
    overflow: hidden;
}

.video-container iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}
<!-- In custom video block template -->
<div class="video-container">
    <iframe
        src="https://www.youtube.com/embed/<?php echo $videoID; ?>"
        frameborder="0"
        allowfullscreen
    ></iframe>
</div>

C. Social Media Embeds

For Twitter, Instagram, Facebook embeds:

/* Reserve space for social embed */
.social-embed-wrapper {
    min-height: 500px;
    background: #f9f9f9;
}

Use placeholder while loading:

<div class="social-embed-wrapper">
    <div class="placeholder">Loading...</div>
    <!-- Social embed code -->
</div>

5. Block Edit Mode vs. View Mode

Content that looks different in edit mode vs. view mode can cause shifts.

Problem: Blocks styled differently when editing vs. viewing, causing unexpected layout.

Solutions:

A. Consistent Styling

/* Ensure consistent block styling */
.ccm-block-edit .custom-block,
.custom-block {
    min-height: 200px;
    padding: 20px;
}

B. Test in View Mode

Always test pages in view mode (logged out or preview):

<?php
// In template, check if in edit mode
if ($c->isEditMode()) {
    // Apply edit-specific styles carefully
}
?>

Carousels often cause layout shifts during initialization.

Problem: Carousel shows all slides stacked, then shifts when JavaScript initializes.

Solutions:

A. Hide Non-Active Slides with CSS

/* Hide slides until carousel initializes */
.carousel-block .slide {
    display: none;
}

.carousel-block .slide.active,
.carousel-block.initialized .slide {
    display: block;
}

/* Reserve height for carousel */
.carousel-block {
    min-height: 400px;
}

B. Initialize Before Page Render

Load carousel in <head> instead of at bottom:

<?php
// In template
$this->requireAsset('javascript', 'carousel');
?>

C. Use CSS-Only Carousels

Consider CSS-only solutions to avoid JavaScript initialization delays:

/* Simple CSS carousel using scroll-snap */
.css-carousel {
    display: flex;
    overflow-x: scroll;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
}

.css-carousel .slide {
    flex: 0 0 100%;
    scroll-snap-align: start;
}

7. Lazy-Loaded Images

Incorrectly implemented lazy loading can cause layout shifts.

Problem: Lazy-loaded images without dimensions shift layout when they load.

Solutions:

A. Always Include Dimensions

<?php
$image = $this->controller->get('image');
$thumbnail = $image->getThumbnail('large');
?>
<img
    src="<?php echo $thumbnail->src; ?>"
    width="<?php echo $thumbnail->width; ?>"
    height="<?php echo $thumbnail->height; ?>"
    loading="lazy"
    alt="<?php echo $image->getTitle(); ?>"
>

B. Use Placeholder Images

Low-quality image placeholders (LQIP):

<?php
$placeholder = $image->getThumbnail('tiny'); // 20x20 thumbnail
$full = $image->getThumbnail('large');
?>
<img
    src="<?php echo $placeholder->src; ?>"
    data-src="<?php echo $full->src; ?>"
    width="<?php echo $full->width; ?>"
    height="<?php echo $full->height; ?>"
    class="lazy-image"
    alt="<?php echo $image->getTitle(); ?>"
>

8. Sticky Headers and Footers

Sticky elements can cause shifts when they activate.

Problem: Header becomes fixed/sticky, shifting content below.

Solutions:

A. Reserve Space for Sticky Header

/* When header becomes sticky, add padding to body */
body.sticky-header {
    padding-top: 80px; /* Header height */
}

header.sticky {
    position: fixed;
    top: 0;
    width: 100%;
    z-index: 1000;
}
// Add class when scrolling
window.addEventListener('scroll', function() {
    if (window.scrollY > 100) {
        document.body.classList.add('sticky-header');
        document.querySelector('header').classList.add('sticky');
    } else {
        document.body.classList.remove('sticky-header');
        document.querySelector('header').classList.remove('sticky');
    }
});

B. Use transform Instead of position

/* Less likely to cause layout shift */
header {
    transform: translateY(0);
    transition: transform 0.3s ease;
}

header.sticky {
    transform: translateY(-100%);
}

9. Form Blocks

Forms with validation messages can cause layout shifts.

Problem: Error messages appear, pushing content down.

Solutions:

A. Reserve Space for Error Messages

/* Reserve space for validation message */
.form-group {
    margin-bottom: 30px; /* Includes space for error */
}

.error-message {
    min-height: 20px;
    margin-top: 5px;
}
<!-- In form block template -->
<div class="form-group">
    <input type="text" name="email" required>
    <div class="error-message">
        <!-- Error appears here without shifting layout -->
    </div>
</div>

B. Use Absolute Positioning for Errors

.form-group {
    position: relative;
    margin-bottom: 20px;
}

.error-message {
    position: absolute;
    bottom: -20px;
    left: 0;
    font-size: 12px;
    color: red;
}

10. Grid and Flexbox Layouts

Improperly configured grid layouts can shift.

Problem: Grid items adjust size when content loads.

Solutions:

A. Set Explicit Grid Template Rows/Columns

.grid-container {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(auto-fill, 300px); /* Fixed row height */
    gap: 20px;
}

B. Use min-height on Grid Items

.grid-item {
    min-height: 300px;
    display: flex;
    flex-direction: column;
}

Testing & Monitoring

Test CLS

Tools:

  1. PageSpeed Insights - Field and lab CLS data
  2. Chrome DevTools - Performance tab with Layout Shift events
  3. WebPageTest - Detailed CLS analysis
  4. Web Vitals Extension - Real-time CLS monitoring

Test Multiple Pages:

  • Homepage
  • Blog posts
  • Form pages
  • Landing pages with dynamic content

Test Different Scenarios:

  • Slow 3G connection (to see loading shifts)
  • Fast connection
  • Mobile devices
  • Different viewport sizes

Debug CLS in Chrome DevTools

  1. Open DevTools (F12)
  2. Go to Performance tab
  3. Click Record
  4. Reload page
  5. Stop recording
  6. Look for red Layout Shift events in timeline
  7. Click on events to see which elements shifted

Monitor CLS Over Time

Google Search Console:

Chrome User Experience Report (CrUX):

  • Field data from real users
  • 28-day rolling average

Quick Wins Checklist

Start here for immediate CLS improvements:

  • Add width/height to all images
  • Use aspect-ratio boxes for responsive images
  • Set font-display: swap for web fonts
  • Reserve space for ads and embeds
  • Add min-height to dynamic content blocks
  • Fix carousel initialization
  • Reserve space for form error messages
  • Use CSS transitions instead of JavaScript animations
  • Test in Chrome DevTools Performance tab
  • Add explicit grid dimensions

When to Hire a Developer

Consider hiring a Concrete CMS developer if:

  • CLS consistently over 0.25 after basic fixes
  • Complex custom blocks need optimization
  • Theme requires significant restructuring
  • JavaScript-heavy blocks causing shifts
  • Need custom block development with CLS in mind

Find Developers: Concrete CMS Marketplace

Next Steps

For general CLS optimization strategies, see CLS Optimization Guide.