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

Fix CLS Issues on Tinacms (Layout Shift)

Stabilize TinaCMS layouts by sizing Git-backed media images, reserving MDX component render space, and preloading static site fonts.

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. TinaCMS sites generate CLS from images in Git-backed content loading without dimensions, custom MDX components rendering at variable heights after hydration, the TinaCMS editing toolbar appearing in edit mode, and web font loading in the frontend framework.

TinaCMS-Specific CLS Causes

  • Markdown/MDX images without dimensions -- images referenced in Markdown (![alt](image.jpg)) render without width/height attributes
  • MDX component hydration -- custom React components in MDX (accordions, tabs, carousels) render a loading state then swap to full content
  • TinaCMS edit toolbar -- the editing toolbar pushes page content down when entering edit mode
  • Dynamic content from Tina queries -- pages using useTina() hook may re-render when edit data loads
  • Font loading -- custom fonts in your Next.js/Astro theme cause text reflow

Fixes

1. Size Images in MDX Content

Override the default Markdown image renderer:

// In your MDX component mapping
export const mdxComponents = {
  img: ({ src, alt, ...props }) => {
    // If using Next.js Image
    return (
      <div style={{ aspectRatio: '16/9', overflow: 'hidden', background: '#f0f0f0' }}>
        <img
          src={src}
          alt={alt || ''}
          loading="lazy"
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
          {...props}
        />
      </div>
    );
  },
};

Or with Next.js Image for automatic sizing:

import Image from 'next/image';

export const mdxComponents = {
  img: ({ src, alt }) => (
    <Image
      src={src}
      alt={alt || ''}
      width={800}
      height={450}
      loading="lazy"
      style={{ width: '100%', height: 'auto' }}
    />
  ),
};

2. Reserve Space for MDX Components

// Wrap interactive MDX components with min-height containers
const Accordion = dynamic(() => import('./Accordion'), {
  ssr: false,
  loading: () => <div style={{ minHeight: 200, background: '#f8f8f8' }} />,
});

const Tabs = dynamic(() => import('./Tabs'), {
  ssr: false,
  loading: () => <div style={{ minHeight: 300, background: '#f8f8f8' }} />,
});

const CodePlayground = dynamic(() => import('./CodePlayground'), {
  ssr: false,
  loading: () => <div style={{ minHeight: 400, background: '#1e1e1e' }} />,
});
/* Generic MDX component containment */
.mdx-component {
  contain: layout;
  min-height: 100px;
}

/* Specific component types */
.mdx-accordion { min-height: 200px; }
.mdx-tabs { min-height: 300px; }
.mdx-gallery { min-height: 400px; }
.mdx-code-playground { min-height: 400px; }

3. Handle TinaCMS Edit Mode Toolbar

/* Prevent edit toolbar from pushing content */
[data-tina-toolbar] {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 99999;
}

/* Only add padding in edit mode */
body.tina-edit-mode {
  padding-top: 48px; /* Toolbar height */
}

4. Stabilize Tina Query Re-renders

// Use useTina with stable initial data to prevent layout shifts
import { useTina } from 'tinacms/dist/react';

export function Page({ data: initialData }) {
  const { data } = useTina({
    query: '...', // your GraphQL query
    variables: { relativePath: '...' },
    data: initialData, // SSG/ISR data prevents flash
  });

  return (
    <article>
      {/* Content renders immediately from initialData,
          then seamlessly updates if editing */}
      <h1>{data.post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: data.post.body }} />
    </article>
  );
}

5. Preload Fonts

// In your Next.js layout or _document.tsx
import { Inter } from 'next/font/google';

// Next.js font optimization (automatic preload + font-display: swap)
const inter = Inter({ subsets: ['latin'], display: 'swap' });

// Or manually in Astro
// In your layout <head>:
<link rel="preload" href="/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%;
}

Measuring CLS on TinaCMS Sites

  1. Chrome DevTools Performance tab -- record page load and filter for layout-shift entries, especially during React hydration
  2. Test MDX-heavy pages -- pages with many custom MDX components have the highest CLS risk
  3. Test edit mode vs. production -- verify the TinaCMS toolbar does not cause CLS in edit mode
  4. Next.js Web Vitals -- enable experimental.webVitalsAttribution to get per-page CLS reporting

Analytics Script Impact

  • TinaCMS has no production-side analytics injection (edit-time only)
  • Use Next.js Script component with strategy="afterInteractive" for analytics
  • In Astro, use <script> tags with defer in your layout
  • Avoid analytics tools that inject visible elements before hydration completes