CLS Shopify 2.0

Fix CLS in Shopify

Cumulative Layout Shift (CLS) measures how much page content moves unexpectedly as the page loads. In Shopify stores, the most common causes are product images loading without reserved space, custom fonts swapping from a fallback typeface to the loaded font, announcement bars that resize when their content renders, and variant selectors or pricing blocks injected by JavaScript after the initial HTML is painted. A CLS score above 0.1 signals a noticeably jumpy page experience that frustrates customers and reduces conversion rates. These five fixes address the main sources of layout shift specific to Shopify's architecture.

Expected results

Before

0.31

CLS (Poor) -- images without dimensions, font swap shifts, unstable header

After

0.05

CLS (Good) -- reserved image space, swap-safe fonts, stable header height

Step-by-step fix

Set explicit dimensions on all product images

Images without width and height attributes are the most common single source of CLS in any website, and Shopify stores are no exception. When the browser encounters an image without dimensions, it cannot reserve space for it -- the image loads, takes up space, and pushes everything below it down. In Shopify Liquid, the image_tag filter accepts width and height parameters that output these as HTML attributes. For product grids where images may have varied aspect ratios, use CSS aspect-ratio on the image container so the browser maintains consistent space regardless of the image's natural dimensions. Every product image, collection thumbnail, and blog featured image should follow this pattern.

Liquid + CSS -- Product grid image dimensions
{%- comment -%} sections/main-collection-grid.liquid {%- endcomment -%}
<div class="product-grid">
  {%- for product in collection.products -%}
    <div class="product-card">
      <div class="product-card__image-wrapper">
        {{ product.featured_image | image_tag:
          width: 600,
          height: 600,
          loading: 'lazy',
          class: 'product-card__image',
          alt: product.featured_image.alt | default: product.title
        }}
      </div>
    </div>
  {%- endfor -%}
</div>

/* CSS: reserve space with aspect-ratio */
.product-card__image-wrapper {
  aspect-ratio: 1 / 1;       /* square product images */
  overflow: hidden;
  background: var(--color-surface);  /* neutral placeholder color */
  border-radius: 4px;
}

.product-card__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

Reserve space for variant selectors and dynamic pricing

Product pages in Shopify are a particularly common source of CLS because the variant selector (color swatches, size dropdowns), dynamic pricing, and availability text are often rendered or updated by JavaScript after the initial HTML loads. When these elements appear or change size, they push the Add to Cart button and surrounding content down the page. The solution is to set a min-height on the containers that hold these JavaScript-populated elements. This tells the browser to hold the space even before the JavaScript runs, preventing the shift. Use your browser's DevTools to measure the typical rendered height of each element and set a min-height that covers the tallest expected variant configuration.

CSS -- Product form CLS prevention
/* Reserve space for variant selectors before JS populates them */
.product-form__variants {
  min-height: 60px;  /* adjust to your theme's variant selector height */
}

/* Reserve space for dynamic pricing block */
.product-form__price {
  min-height: 40px;
}

/* Reserve space for availability text (In stock / Sold out) */
.product-form__availability {
  min-height: 24px;
}

/* Prevent currency switcher from causing price text reflow */
.product-price-amount {
  display: inline-block;
  min-width: 4ch;    /* holds space for prices like "$9.99" */
}

/* In theme.liquid or your custom CSS file -- add to Assets */
/* For Dawn theme: add to assets/section-main-product.css */

Preload fonts with font-display:swap

Font-induced CLS occurs when a page renders with a system fallback font, then switches to the custom typeface once it loads. The fallback and custom fonts have different character widths and line heights, which causes all text on the page to reflow. Shopify's native font library handles this automatically for fonts selected in the Theme Customizer -- it generates rel="preload" links and applies font-display:swap. If you are using custom fonts loaded via a <link> tag in theme.liquid, you need to add the preload hints manually and ensure font-display:swap is set in your CSS @font-face declarations. Avoid loading fonts from Google Fonts via a stylesheet tag -- use Shopify's font_face filter for fonts in the Shopify library, or host custom font files in your theme's assets folder to avoid cross-origin latency.

Liquid + CSS -- theme.liquid font preloading
{%- comment -%}
  layout/theme.liquid: Shopify native font preloading
  (Dawn and OS 2.0 themes handle this automatically)
{%- endcomment -%}
{%- assign body_font = settings.type_body_font -%}
{%- assign heading_font = settings.type_header_font -%}

<link
  rel="preload"
  as="font"
  href="{{ body_font | font_url }}"
  type="font/woff2"
  crossorigin
>
<link
  rel="preload"
  as="font"
  href="{{ heading_font | font_url }}"
  type="font/woff2"
  crossorigin
>

<style>
  /* Shopify generates this via font_face filter */
  {{ body_font | font_face: font_display: 'swap' }}
  {{ heading_font | font_face: font_display: 'swap' }}

  :root {
    --font-body: {{ body_font.family }}, {{ body_font.fallback_families }};
    --font-heading: {{ heading_font.family }}, {{ heading_font.fallback_families }};
  }
</style>

Stabilize header and announcement bar

The announcement bar at the top of many Shopify stores is a frequent CLS culprit. If the bar's content (promotional text, countdown timer, free shipping threshold) is loaded asynchronously -- either by a Shopify app or by JavaScript reading a metafield -- the bar appears after the initial render and pushes the entire page down. Similarly, if your theme's header changes height when it transitions from a transparent hero state to a solid background state, that transition causes CLS. The fix involves two CSS properties: set a fixed height on the announcement bar container so the browser reserves its space from the start, and add contain: layout to prevent the bar's internal changes from triggering a full-page reflow. For the header, pin its height with a fixed pixel value or use min-height if the content varies.

CSS -- Announcement bar and header stabilization
/* Announcement bar: reserve space and isolate layout */
.announcement-bar {
  height: 40px;              /* fix to your bar's actual rendered height */
  contain: layout;           /* layout changes stay inside this element */
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

/* If bar uses a rotating message carousel, fix height here too */
.announcement-bar__message {
  height: 40px;
  line-height: 40px;
}

/* Header: fixed height prevents transparent-to-solid transition shift */
.site-header {
  height: 60px;              /* adjust to your theme's header height */
  position: sticky;
  top: 0;
  z-index: 100;
}

/* For Dawn: override in assets/section-header.css */
.header-wrapper {
  min-height: 60px;
}

Handle lazy-loaded sections without shift

Shopify 2.0 supports lazy loading entire sections below the fold, which is great for performance but introduces a new CLS source: if no space is reserved for the section before it loads, other content jumps when the section appears. Product collection grids are the most common case -- the grid loads after the hero, and if no height is reserved, the footer and other below-fold elements shift upward then downward as the grid populates. Use CSS aspect-ratio on each product card image container so that even before the images load, the correct amount of vertical space is held. For sections that use JavaScript to fetch and render data (such as recently viewed products or personalized recommendations), implement a CSS skeleton loader: a fixed-height container with a subtle background color that acts as a placeholder until the real content arrives.

CSS + Liquid -- Skeleton loading for collection grids
/* Collection grid: stable aspect-ratio containers */
.collection-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 16px;
}

.collection-grid__item {
  aspect-ratio: 1 / 1;
  background: #f3f4f6;       /* skeleton placeholder color */
  border-radius: 4px;
  overflow: hidden;
  position: relative;
}

/* Skeleton shimmer animation */
.collection-grid__item::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}

/* Once image loads, remove skeleton */
.collection-grid__item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

Quick checklist

  • All product images use image_tag with explicit width and height parameters
  • CSS aspect-ratio applied to all image grid containers
  • Product form containers have min-height set for variant selector and pricing areas
  • Fonts use Shopify's font_face filter with font-display: swap
  • Announcement bar has a fixed height and contain: layout applied
  • Collection grids use skeleton loading containers with stable dimensions before images populate

Frequently asked questions

The most common causes of CLS in Shopify stores are images without explicit width and height attributes causing reflow when they load, fonts swapping from a fallback typeface to the custom font causing text to reflow, announcement bars and headers that change height when content loads, variant selectors and pricing blocks injected by JavaScript after the initial HTML, and third-party app pop-ups or banners injecting into the page after the initial render.

Yes. When a customer selects a product variant (color, size, material), Shopify themes update the product image, price, and availability via JavaScript. If the updated image has a different aspect ratio or the price text changes length, these updates cause layout shifts. Setting explicit aspect ratios on product images and reserving minimum space for price and availability containers prevents these variant-driven shifts from counting against your CLS score.

Currency switcher apps are a known CLS source. When the app loads and updates prices from one currency to another, the new price text may be longer or shorter than the original, causing surrounding elements to reflow. To minimize this, set a fixed min-width or min-height on price containers and use a monospace or fixed-width font for prices if possible. Ensure the currency switcher loads during the page's initial render rather than swapping values after the user can see the page.

Dawn is currently Shopify's best theme for CLS. It sets explicit dimensions on all product images throughout, uses CSS custom properties for theming rather than JavaScript-injected styles, and manages font loading through Shopify's native font library with font-display swap behavior. Dawn also avoids jQuery-based DOM manipulation that older themes like Debut and Brooklyn use, which frequently causes layout shifts on page load and user interactions.

Run a Lighthouse audit from Chrome DevTools or PageSpeed Insights. The Lighthouse report highlights the elements causing layout shift and their individual shift scores. For real-time debugging, open the Chrome DevTools Performance panel, enable the Layout Shift Regions checkbox in the Rendering tab, then scroll through the page -- shifting elements will highlight in blue. For field data from real customers, add the web-vitals library to your theme.liquid to capture and report actual CLS values.

Set up real-user monitoring using the web-vitals JavaScript library (1.5KB). Send CLS data to your analytics platform (Google Analytics 4, custom endpoint). The attribution build identifies exactly which element caused each layout shift. For Shopify, also monitor CLS after route transitions, as client-side navigation can trigger additional shifts not captured in initial page load.

Continue learning