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 (
) render withoutwidth/heightattributes - 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
- Chrome DevTools Performance tab -- record page load and filter for layout-shift entries, especially during React hydration
- Test MDX-heavy pages -- pages with many custom MDX components have the highest CLS risk
- Test edit mode vs. production -- verify the TinaCMS toolbar does not cause CLS in edit mode
- Next.js Web Vitals -- enable
experimental.webVitalsAttributionto get per-page CLS reporting
Analytics Script Impact
- TinaCMS has no production-side analytics injection (edit-time only)
- Use Next.js
Scriptcomponent withstrategy="afterInteractive"for analytics - In Astro, use
<script>tags withdeferin your layout - Avoid analytics tools that inject visible elements before hydration completes