Loading states should reserve the space the final content will occupy, so the page does not jump when the data arrives. Layout shifts happen when a placeholder is shorter than the content it replaces, or when new content pushes everything below it down.
Why layout shifts happen
When a loading placeholder is smaller than the real content, the real content shoves the page down the moment it renders. Browsers measure this as Cumulative Layout Shift, and it makes a page feel broken even when it loads fast. The fix is always the same: make the placeholder the same size as the thing it stands in for.
Reserve space for images and media
Images are the most common cause. Give every image explicit width and height, or an aspect ratio, so the browser allocates the box before the file loads.
export default function Avatar({ src, name }) {
return (
<img
src={src}
alt={name}
width={48}
height={48}
className="rounded"
/>
);
}The browser reserves a 48 by 48 box immediately, so the image popping in later moves nothing. Modern browsers derive the aspect ratio from those two attributes on their own. For responsive images, set aspect-ratio in CSS and let the width flex.
Match the skeleton to the content
A skeleton should copy the shape of the content it stands in for. Same row height, same spacing, same width. A list skeleton is a column of bars with the same height and gap as the final rows.
function PostsSkeleton() {
return (
<ul className="skeleton-list" aria-hidden="true">
<li />
<li />
<li />
</ul>
);
}Each li gets a fixed height and the same gap as a real post row in CSS. Keep the bars the same width as real content too, so nothing reflows horizontally.
The aria-hidden keeps the decorative bars out of the accessibility tree. When the real list replaces the skeleton, the page height does not change.
Make the skeleton the Suspense fallback so React shows it while the content loads:
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>Because the skeleton matches the list height, the reveal is a same-size swap instead of a jump. For how Suspense picks reveal moments, see React Suspense explained.
Do not inject above the viewport
Reserve space below existing content, or hold the shift in place. If a loading state adds a banner or a row above the fold, everything under it jumps.
New rows should append at the bottom of an existing list, never above the item the user is reading. Prefer inline placeholders that occupy the same box as the content they replace.
A skeleton is only the loading branch of a full async UI, so pair it with distinct empty and error states. For skeletons that announce progress accessibly, see accessible loading states, skeletons, and spinners in React.
Rune AI
Key Insights
- Reserve space for images with width, height, or aspect-ratio.
- Make skeletons match the final content height and spacing.
- Use the skeleton as the Suspense fallback.
- Do not inject new content above the viewport while loading.
- Distinguish loading from empty and error states.
Frequently Asked Questions
Why does my page jump when a list finishes loading?
Should I set width and height on every image?
Conclusion
Loading states should reserve the space the real content will occupy. Reserve image boxes, match skeletons to final dimensions, and keep the skeleton as the Suspense fallback.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.