Shimmer UI
Shimmer UI displays temporary placeholder shapes that resemble the real interface while data is loading.
Loading starts
↓
Shimmer placeholders appear
↓
Data arrives
↓
Real content replaces the placeholders
One-line idea: Show the shape of the upcoming content instead of leaving the screen empty.
Core Concepts
The Problem with Plain Loading Text
A basic loading state may show only:
<p>Loading...</p>
This tells the user that something is happening, but the rest of the page remains empty.
Users may feel that:
- the application is slow
- nothing is happening
- content is missing
- the interface is incomplete
The real UI also appears suddenly when the request completes.
Blank screen
↓
Sudden content appearance
Shimmer UI provides a more stable transition:
Placeholder layout
↓
Real content replaces it
What Is Shimmer UI?
Shimmer UI is a loading state that imitates the structure of the real content.
Instead of rendering empty space:
<Card />
<Card />
<Card />
Render temporary placeholder cards:
<ShimmerCard />
<ShimmerCard />
<ShimmerCard />
Each placeholder represents the expected image, heading, text or action area of the real card.
Skeleton and Shimmer
The skeleton is the placeholder structure. The shimmer is the moving highlight that makes the loading state feel active.
Skeleton -> shape of the future content
Shimmer -> animated highlight across that shape
Example structure:
ShimmerCard
├── ImagePlaceholder
├── TitlePlaceholder
└── TextPlaceholder
The skeleton should closely match the dimensions and arrangement of the real component.
Dummy Cards
Dummy cards temporarily occupy the places where real cards will appear.
While loading:
[ ShimmerCard ][ ShimmerCard ][ ShimmerCard ]
After loading:
[ ProductCard ][ ProductCard ][ ProductCard ]
They do not contain real data. Their purpose is to preserve the expected page structure during loading.
Layout Stability
If the shimmer structure matches the real UI, the page does not need to rearrange dramatically when data arrives.
This helps:
- reduce sudden layout movement
- keep the interface visually stable
- make the transition to real content smoother
- show users where content will appear
Matching placeholder dimensions
↓
Real content fits the same space
↓
Smaller visual shift
Perceived Performance
Shimmer UI mainly improves perceived performance.
It does not make the API respond faster. It makes the waiting experience feel faster and more understandable.
While waiting, users can already see that:
- content is being loaded
- page sections are being prepared
- the layout is available
- the application is responding
| Actual performance | Perceived performance |
|---|---|
| How quickly the data really arrives | How fast the application feels to the user |
| Controlled by network, API and processing time | Improved by clear and stable loading feedback |
Practical Shimmer Patterns
Create a Reusable Shimmer Block
Start with one reusable block that provides the animated background.
export default function ShimmerBlock({ className = "" }) {
return <div className={`shimmer-block ${className}`} />;
}
.shimmer-block {
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 37%, #e5e7eb 63%);
background-size: 400% 100%;
animation: shimmer 1.4s ease infinite;
}
@keyframes shimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: 0 0;
}
}
The gradient moves across the block and produces the shimmer effect.
Build a Card Skeleton
The shimmer card should follow the same basic structure as the real card.
import ShimmerBlock from "./ShimmerBlock";
import "./shimmer.css";
export default function ProductCardShimmer() {
return (
<article className="product-card product-card--shimmer">
<ShimmerBlock className="product-card__image-placeholder" />
<div className="product-card__content">
<ShimmerBlock className="product-card__title-placeholder" />
<ShimmerBlock className="product-card__text-placeholder" />
<ShimmerBlock className="product-card__text-placeholder short" />
</div>
</article>
);
}
.product-card--shimmer {
width: 280px;
overflow: hidden;
border: 1px solid #e5e7eb;
border-radius: 12px;
}
.product-card__image-placeholder {
width: 100%;
height: 180px;
}
.product-card__content {
padding: 16px;
}
.product-card__title-placeholder {
width: 70%;
height: 22px;
margin-bottom: 12px;
border-radius: 4px;
}
.product-card__text-placeholder {
width: 100%;
height: 14px;
margin-bottom: 8px;
border-radius: 4px;
}
.product-card__text-placeholder.short {
width: 55%;
}
Structure comparison:
| Real card | Shimmer card |
|---|---|
| Product image | Image placeholder |
| Product title | Title placeholder |
| Description | Text placeholders |
Render Multiple Dummy Cards
A loading grid should display enough placeholders to represent the incoming list.
import ProductCardShimmer from "./ProductCardShimmer";
export default function ProductGridShimmer({ count = 6 }) {
return (
<div className="product-grid">
{Array.from({ length: count }).map((_, index) => (
<ProductCardShimmer key={index} />
))}
</div>
);
}
The shimmer grid and real product grid should use the same layout container.
export default function ProductGrid({ products }) {
return (
<div className="product-grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
This makes the replacement feel natural because both states occupy a similar layout.
Conditional Rendering
Use loading state to choose between shimmer UI and real UI.
export default function ProductsSection({ isLoading, products }) {
if (isLoading) {
return <ProductGridShimmer count={6} />;
}
return <ProductGrid products={products} />;
}
isLoading = true -> ProductGridShimmer
isLoading = false -> ProductGrid
This is the core shimmer rendering pattern.
Complete Loading Flow
import { useEffect, useState } from "react";
export default function ProductsPage() {
const [products, setProducts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function loadProducts() {
try {
const response = await fetch("/api/products");
const data = await response.json();
setProducts(data);
} finally {
setIsLoading(false);
}
}
loadProducts();
}, []);
return (
<main>
<h1>Products</h1>
{isLoading ? (
<ProductGridShimmer count={6} />
) : (
<ProductGrid products={products} />
)}
</main>
);
}
Component mounts
↓
isLoading is true
↓
Shimmer grid renders
↓
Request completes
↓
Products are stored
↓
isLoading becomes false
↓
Real product grid renders
Shimmer for a Scrollable Row
Scrollable sections should keep their row structure visible while content loads.
export default function ScrollableRowShimmer({ count = 5 }) {
return (
<section className="scrollable-section">
<ShimmerBlock className="section-title-placeholder" />
<div className="scrollable-row">
{Array.from({ length: count }).map((_, index) => (
<ProductCardShimmer key={index} />
))}
</div>
</section>
);
}
.scrollable-row {
display: flex;
gap: 16px;
overflow-x: auto;
}
.scrollable-row .product-card--shimmer {
flex: 0 0 280px;
}
.section-title-placeholder {
width: 220px;
height: 28px;
margin-bottom: 16px;
border-radius: 4px;
}
Instead of showing an empty row, the placeholders indicate how many cards and what layout the user can expect.
Reusable Shimmer Components
Different real components can have matching reusable loading components.
ProductCard -> ProductCardShimmer
Banner -> BannerShimmer
ContentRow -> RowShimmer
Profile -> ProfileShimmer
Example banner placeholder:
export default function BannerShimmer() {
return (
<section className="banner banner--shimmer">
<ShimmerBlock className="banner__title-placeholder" />
<ShimmerBlock className="banner__text-placeholder" />
</section>
);
}
Reusable shimmer components prevent every page from rebuilding the same loading structure.
Match the Real Layout
The placeholder should represent the component that will replace it.
Match these properties where possible:
- width and height
- card count
- spacing and gaps
- border radius
- image position
- number and approximate length of text lines
- grid or scroll direction
Accurate skeleton
↓
Stable replacement
↓
Smoother loading experience
Benefits, Trade-Offs and Common Mistakes
Benefits
- avoids an empty loading screen
- makes progress visually understandable
- improves perceived speed
- keeps the page structure visible
- reduces sudden content appearance
- improves visual consistency during loading
- supports reusable loading states across pages
Trade-offs
| Plain loading text | Shimmer UI |
|---|---|
| Very simple to implement | Requires placeholder components and styles |
| Does not show the incoming structure | Previews the expected layout |
| Can leave large blank areas | Keeps content areas visually occupied |
| Creates a sudden transition | Allows a smoother replacement |
Shimmer UI improves the experience while waiting, but it does not reduce the real API or network time.
Common mistakes
| Mistake | Better approach |
|---|---|
| Using one generic rectangle for every screen | Match placeholders to the real component structure |
| Rendering a different number of shimmer cards | Approximate the expected content count |
| Giving placeholders different dimensions | Reuse the real layout dimensions |
| Creating shimmer markup separately on every page | Build reusable shimmer components |
| Removing the entire layout during loading | Keep grids and scrollable containers in place |
| Showing shimmer after data has arrived | Use loading state for clear conditional rendering |
| Expecting shimmer to reduce API time | Treat it as a perceived-performance improvement |
Interview Revision
Quick Revision Checklist
- Shimmer UI displays placeholder layouts while data loads.
- Placeholder shapes should resemble the real UI.
- Dummy cards keep lists and grids visually occupied.
- A skeleton defines structure; shimmer provides the moving highlight.
- Conditional rendering switches between shimmer and real content.
- Matching dimensions helps reduce layout movement.
- Reusable shimmer components prevent duplicated loading markup.
- Scrollable rows should retain their structure during loading.
- Shimmer improves perceived performance, not actual API speed.
- Real content replaces the placeholders after loading completes.
Frequently Asked Interview Questions
1. What is Shimmer UI?
Shimmer UI is a loading pattern that renders animated placeholder shapes resembling the real interface until the actual data is available.
2. Why use shimmer instead of plain loading text?
Shimmer keeps the expected page structure visible, provides clearer loading feedback and creates a smoother transition to real content.
3. What is the difference between a skeleton and shimmer?
The skeleton is the placeholder structure. Shimmer is the animated highlight moving across that structure.
4. Does shimmer improve actual performance?
No. It does not make the network request faster. It improves perceived performance by making the application feel active and visually prepared.
5. How does shimmer reduce layout movement?
The placeholder reserves approximately the same space as the real content. When data arrives, the real component replaces a similarly sized skeleton.
6. How do you conditionally render a shimmer component?
Track a loading state. Render the shimmer component while loading is true, then render the real component after the data arrives.
7. Why create reusable shimmer components?
Reusable shimmer cards, banners and rows keep loading states consistent and prevent duplicated placeholder markup across different pages.
8. How would you create shimmer for a list?
Render multiple dummy items with the same grid or row container used by the real list. Replace them with real items when loading finishes.
9. What should a good shimmer layout match?
It should approximately match the real component's size, shape, spacing, image area and text-line structure.
Memory Trick
Shape -> Shine -> Swap
Shape the expected content with placeholders
Shine an animated gradient across them
Swap placeholders with real content when data arrives
One-Line Summary
Shimmer UI uses animated skeleton layouts to make data loading feel smoother, clearer and more stable.
Final Mental Model
Data request begins
↓
Loading state becomes true
↓
Skeleton components preserve the layout
↓
Shimmer animation shows activity
↓
Data arrives
↓
Loading state becomes false
↓
Real components replace the placeholders