React

loading.tsx and skeletons without layout jumps

loading.tsx creates a Suspense boundary: how to write a skeleton that matches the final shape of the real content, so it adds no CLS.

Key points

  • `loading.tsx` is a Suspense boundary created by a file convention.
  • The skeleton must match the final height and column count.
  • Below 200 ms of wait, a skeleton is worse than nothing.

loading.tsx wraps a segment in a Suspense boundary and shows that file while the server prepares the page. A skeleton whose height differs from the real content adds CLS, not the impression of speed.

What loading.tsx actually does

The file is syntactic sugar: Next creates a <Suspense fallback={<Loading />}> around the segment. No import, no configuration, and it covers the page plus any child segment without its own loading.tsx.

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    
      {Array.from({ length: 6 }).map((_, i) => (
        
      ))}
    
  );
}

The rule that avoids the jump

The skeleton must occupy the final space, not a plausible one. If the real card is 128 px tall, the skeleton is 128 px. If the grid has three columns at 768 px, the skeleton has the same three columns at the same breakpoint.

ElementWhat to copyWhat to avoid
HeightThe final value in px or in text linesA provisional h-24
GridSame columns and same gapA flex layout unlike the result
TextA plausible number of linesFake words of a different length
ImageThe content aspect-ratioA generic square

When the jump happens anyway

Two recurring causes: a font that changes metrics when it loads, and an image without dimensions. Fix the first with next/font and a fallback metric; fix the second with width and height or aspect-ratio. The details are in layout shift from images without dimensions and in CLS: causes and fixes.

The budget is numeric: CLS below 0.1, measured on 1 November 2026 on a page of the EneUI catalogue, which indexes 111,985 components from 1,472 libraries. The skeleton is part of the budget, not an aesthetic detail.

Skeletons from the catalogue

Three records: the skeleton components with the base shapes, the loading states for spinners and progress, and the dashboard blocks for a grid that holds up while loading.

If the boundary must handle an error and not just the wait, the pattern is in error boundaries in the App Router. The streaming that makes the fallback useful is in streaming and Suspense. Folder layout is in App Router structure.

The limit to budget for

A loading.tsx at the root layout level shows the skeleton even for navigations that last 80 ms: below 200 ms the wait is invisible and the flash is worse than nothing.

Frequently asked questions

Is loading.tsx mandatory?

No. It earns its place when the segment has a real wait. A project without <code>loading.tsx</code> works: the user keeps seeing the previous page until the new one is ready.

How do I stop the skeleton flashing?

Delay when it appears, with a transition, or keep the file only on slow segments. Below 200 ms the wait is not perceived and the fallback makes things feel worse.

Can I animate the skeleton?

Yes, with <code>animate-pulse</code> or a gradient. Avoid animations that move the layout: pulse changes opacity only, not size.

Where do I put loading for one section?

In an explicit <code>&lt;Suspense&gt;</code> around the slow component. <code>loading.tsx</code> covers the whole segment, not a part of it.

Why does the page jump when the content arrives?

Because the skeleton has a different height, or because fonts and images change the metrics. Compare the final box with the fallback box, not by eye.