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

Fix CLS Issues on Buttercms (Layout Shift)

Stabilize ButterCMS layouts by reserving space for API-fetched content, sizing media fields, and preloading fonts in your frontend.

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. ButterCMS is headless, so CLS originates entirely from your frontend implementation -- how you handle async API data, image rendering, and component loading.

ButterCMS-Specific CLS Causes

  • Content popping in after API fetch -- client-side rendering shows a loading state then replaces it with API content, shifting the entire page layout
  • Images without dimensions -- ButterCMS image fields do not include width/height metadata in API responses by default
  • Blog post content variability -- posts with different numbers of images, embeds, and content lengths cause variable-height containers
  • Page builder components -- ButterCMS's Page Type components render at unknown heights until their content loads
  • Third-party embeds in rich text -- YouTube videos, tweets, and other embeds in ButterCMS content fields render without reserved space

Fixes

1. Use SSG/SSR to Eliminate Loading State Shifts

The primary CLS fix for headless CMS: render content server-side so there is no loading-to-content swap:

// CAUSES CLS: Client-side fetch with loading state
function BlogPost({ slug }) {
  const [post, setPost] = useState(null);
  useEffect(() => {
    butter.post.retrieve(slug).then(resp => setPost(resp.data.data));
  }, [slug]);

  if (!post) return <Skeleton />; // Layout shift when replaced
  return <Article post={post} />;
}

// NO CLS: Server-side fetch (Next.js)
export async function getStaticProps({ params }) {
  const resp = await butter.post.retrieve(params.slug);
  return { props: { post: resp.data.data }, revalidate: 60 };
}

function BlogPost({ post }) {
  return <Article post={post} />; // Content rendered immediately
}

2. Set Image Dimensions from ButterCMS Fields

ButterCMS image URLs go through Filestack CDN. Extract dimensions or set known aspect ratios:

// Helper to add dimensions to ButterCMS images
function ButterImage({ url, alt, width = 800, aspectRatio = '16/9', eager = false }) {
  if (!url) return null;

  const optimizedUrl = `${url}?w=${width}&q=80&auto=format`;

  return (
    <div style={{ aspectRatio, overflow: 'hidden', background: '#f0f0f0' }}>
      <img
        src={optimizedUrl}
        alt={alt || ''}
        width={width}
        height={Math.round(width / (16/9))}
        loading={eager ? 'eager' : 'lazy'}
        style={{ width: '100%', height: '100%', objectFit: 'cover' }}
      />
    </div>
  );
}

// Usage in blog post template
function FeaturedImage({ post }) {
  return (
    <ButterImage
      url={post.featured_image}
      alt={post.featured_image_alt}
      width={1200}
      aspectRatio="16/9"
      eager={true}
    />
  );
}

3. Stabilize Page Builder Component Layouts

ButterCMS Page Types define components with different field sets. Set minimum heights per component type:

// Component renderer with CLS prevention
const COMPONENT_MIN_HEIGHTS = {
  hero_section: 500,
  feature_grid: 400,
  testimonial_slider: 300,
  cta_banner: 200,
  content_block: 150,
};

function PageComponent({ component, type }) {
  const minHeight = COMPONENT_MIN_HEIGHTS[type] || 100;

  return (
    <section
      style={{
        minHeight: `${minHeight}px`,
        contain: 'layout',
      }}
    >
      <ComponentRenderer component={component} type={type} />
    </section>
  );
}

4. Handle Rich Text Embeds

ButterCMS's rich text editor allows embedding videos and third-party content. Process the HTML to add dimensions:

// Post-process ButterCMS HTML content to add embed dimensions
function processButterContent(html) {
  if (!html) return '';

  // Add aspect ratio to iframes (YouTube, Vimeo)
  html = html.replace(
    /<iframe([^>]*)>/g,
    '<div style="aspect-ratio:16/9;contain:layout;"><iframe$1 style="width:100%;height:100%;border:0;">'
  );
  html = html.replace(/<\/iframe>/g, '</iframe></div>');

  // Add dimensions to images without them
  html = html.replace(
    /<img(?![^>]*width)([^>]*)>/g,
    '<img$1 style="aspect-ratio:16/9;width:100%;height:auto;object-fit:cover;">'
  );

  return html;
}

// Use in your post component
function PostBody({ content }) {
  return (
    <div
      className="post-content"
      dangerouslySetInnerHTML={{ __html: processButterContent(content) }}
    />
  );
}

5. Preload Fonts in Your Frontend

<!-- In your document head -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preload" href="/fonts/body-font.woff2" as="font" type="font/woff2" crossorigin />
@font-face {
  font-family: 'BodyFont';
  src: url('/fonts/body-font.woff2') format('woff2');
  font-display: swap;
  size-adjust: 104%;
}

Measuring CLS on ButterCMS

  1. Chrome DevTools Performance tab -- record page load and look for layout-shift entries. Common culprits are the transition from loading skeleton to real content.
  2. Compare CSR vs. SSR -- if you see CLS from content loading in, switching to SSR/SSG will eliminate it
  3. Test blog posts with different content -- posts with many images, embeds, or variable-length content expose CLS issues that shorter posts hide
  4. Web Vitals monitoring -- add real-user monitoring since lab tests may not catch CLS from lazy-loaded content:
// Track CLS in production
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      // Send to analytics
      gtag('event', 'cls', { value: entry.value });
    }
  }
}).observe({ type: 'layout-shift', buffered: true });

Analytics Script Impact

Since ButterCMS is headless, analytics CLS depends on your frontend framework:

  • Next.js -- use next/script with strategy="afterInteractive" to avoid injecting scripts that shift content
  • Consent managers -- OneTrust, Cookiebot banners must use position: fixed overlay; do not push page content down
  • A/B testing tools -- Optimizely, VWO modify DOM and cause CLS; prefer server-side testing with ButterCMS's API-based content variants