CLS on Next.js 15 landings: stable shadcn/ui layout — KEL IT
Websites 6 min read

CLS on Next.js 15 SEO landings: shadcn/ui without layout jump

You scroll a service landing page, tap “Get a quote,” and the button slides down as a cookie banner appears. The click lands on the privacy policy link instead. Google records Cumulative Layout Shift (CLS) above 0.1. LCP and INP can look fine in lab tests while Page Experience stays incomplete—Core Web Vitals are a set of three field metrics, not a pick-one checklist.

On Next.js 15 landings built with shadcn/ui and Radix primitives, shifts often hide on fast local Wi‑Fi but show up on real 4G: toasts, modals that remove the scrollbar, lazy avatars in testimonials, third-party chat widgets. This article focuses on practical layout stability for the App Router—not a generic performance overview.

Why CLS matters beyond the score

CLS sums unexpected movement of visible content during the page lifetime. Google grades the 75th percentile of field data (CrUX) per URL or URL group.

RatingCLSCommon landing causes
Good≤ 0.1Sized media, fonts, UI chrome
Needs improvement0.1–0.25Cookie bars in document flow
Poor> 0.25Late web fonts, ads, unsized embeds

For conversion-focused pages, CLS is a business metric: moving CTAs directly lose leads. For SEO, it completes Page Experience alongside LCP and INP on competitive queries.

shadcn/ui and Radix: frequent shift sources

Copied shadcn components are accessible by default but not automatically layout-stable.

Toasts (Sonner). Mount <Toaster /> in the root layout with position="bottom-right" so notifications sit in a fixed layer, not pushing the footer when the first toast appears.

Dialog / Sheet / Popover. Radix locks body scroll by setting overflow: hidden, which removes the scrollbar and shifts content horizontally (~15px on Windows). Add to global CSS:

html {
  scrollbar-gutter: stable;
}

Cards and skeletons. Testimonial grids without min-height grow when API text arrives and shove the hero CTA. Match skeleton height to the final card (min-h-[280px] or equivalent).

Avatars. Keep explicit h-10 w-10 shrink-0 on Avatar wrappers so image load does not resize the row.

FAQ Accordion. Avoid client-only defaultValue from useSearchParams on first paint if SSR HTML does not match—opening a section after hydration shifts everything below the fold.

If you need a similar build — message on Telegram.

Fonts and text blocks with next/font

Font swap from fallback to webfont is a classic CLS source on text-heavy heroes.

import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
  adjustFontFallback: true,
  weight: ['400', '600', '700'],
});

adjustFontFallback: true aligns system fallback metrics with Inter, reducing shift on swap. Limit weights to what the page actually uses.

Reserve vertical space for responsive headlines when line count changes by breakpoint:

<h1 className="min-h-[2.5em] text-4xl font-bold leading-tight md:min-h-[1.2em] md:text-5xl">
  Next.js 15 landing pages for search and conversion
</h1>

Lucide icons inside shadcn Button should use className="size-4 shrink-0" so SVGs do not pop from 0×0 to final size after CSS loads.

Media, embeds, and third parties

Wrap next/image with fill in a parent that has aspect-* or explicit width/height. A zero-height parent until load is a top CLS mistake.

Booking widgets, maps, and video iframes need a reserved box:

<div className="relative aspect-[16/9] w-full max-w-2xl">
  <iframe title="Book a call" src="https://cal.com/embed/..." className="absolute inset-0 h-full w-full border-0" loading="lazy" />
</div>

Cookie consent must be position: fixed at the bottom—not inserted above the footer in normal flow. Load analytics with next/script and strategy="afterInteractive", not blocking tags in <head>.

Chat widgets (Intercom, Crisp) should reserve a fixed corner slot before the SDK injects its button, or defer init until idle/scroll on strict CLS budgets.

Streaming, PPR, and motion

Partial Prerendering ships a static shell fast. If the shell’s skeleton is 200px tall and the streamed block is 600px, CLS is guaranteed. Stream below-the-fold only; keep hero, H1, and primary CTA in the static shell with accurate dimensions.

loading.tsx for a route should mirror the final grid—not a full-page spinner that collapses when content arrives.

framer-motion: animating opacity and transform is safe; animating height, margin, or layout-affecting y in document flow causes shifts. Pair motion guidance with INP optimization—do not trade one vital for another.

A/B copy that changes headline line count should use server-side assignment or min-height for the tallest variant, not a client-only swap after paint.

Measurement and release checklist

  1. Search Console → Core Web Vitals → CLS (mobile first).
  2. Chrome DevTools → Performance → Layout Shift regions and culprits.
  3. Lighthouse → “Avoid large layout shifts” for lab debugging.
  4. @vercel/speed-insights for post-deploy field trends.

Before release:

  • scrollbar-gutter: stable
  • Sized images and aspect-ratio containers
  • next/font with adjustFontFallback, no CSS @import fonts
  • Fixed-layer toasts, cookies, chat chrome
  • Skeletons match final section heights
  • No above-the-fold layout change on first client render
ChangeTypical CLS impact
Top promo bar without reserved heightShifts entire page
Google Fonts <link> instead of next/fontLate swap shift
Minimal spinner → heavy streamed sectionLarge fold jump

Need help? Telegram → or vic.kell@ya.ru

FAQ

Lab CLS is good but Search Console is Poor—why? CrUX reflects real devices and networks. Fix culprits from field reports; use Lighthouse to reproduce specific elements.

Is manual size-adjust needed with next/font? Rarely—adjustFontFallback covers most Google font cases.

Dialog still shifts the page. Confirm scrollbar-gutter: stable and no double body padding from overlapping modal libraries.

GDPR cookie banner without killing CLS? Fixed bottom bar with constant height; never push main content when it appears.

Autoplay carousel? Equal min-height per slide or line-clamp so slide changes do not resize the section.

Closing

CLS on a Next.js 15 landing is layout discipline: anything that appears or resizes after first paint either reserves space upfront or lives in a fixed overlay outside document flow. shadcn/ui works well when toasts, modals, media, and streamed sections are sized intentionally.

Together with LCP (hero, fonts) and INP (lean client JS), holding CLS at or below 0.1 on mobile CrUX completes Core Web Vitals for service landings in 2026—usually within one focused layout sprint when you debug from DevTools Layout Shift entries, not only a green Lighthouse run on a developer laptop.

KEL IT

Need a custom solution?

I build these types of projects professionally. Telegram bots, Mini Apps, websites, mobile and desktop applications. Tell me about your project and I'll get back to you with a plan.