No one likes staring at a blank screen while waiting for data to load.
When we show nothing (or just a spinner), it creates uncertainty — users wonder if something’s broken. A better solution is to use skeleton loaders: visual placeholders that mimic the final layout while content is loading.
Today I'll show you how to build simple and reusable skeleton loaders using styled-components.
✅ The Problem: Empty states while loading
Imagine this:
You’re building a list of cards that loads from an API. During loading, the page looks like this:
[ Loading... ]
But what users really want is to see structure — something like:
[ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ]
[ ]
[ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ]
[ ▓▓▓▓▓▓▓▓▓ ]
Skeleton loaders give a visual cue of what’s coming and make the app feel faster.
💡 The Solution: Skeleton components with styled-components
Let’s build a reusable Skeleton
component that can be styled to match any layout.
Step 1: Create a simple Skeleton
block
import styled, { keyframes } from 'styled-components';
const shimmer = keyframes`
0% { background-position: -1000px 0; }
100% { background-position: 1000px 0; }
`;
export const Skeleton = styled.div`
background: linear-gradient(
90deg,
#eee 25%,
#f5f5f5 50%,
#eee 75%
);
background-size: 1000px 100%;
animation: ${shimmer} 1.5s infinite linear;
border-radius: 4px;
`;
Now we can apply it anywhere!
Step 2: Use it in a list layout
export const CardSkeleton = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Skeleton style={{ width: '100%', height: '180px' }} />
<Skeleton style={{ width: '60%', height: '20px' }} />
<Skeleton style={{ width: '40%', height: '16px' }} />
div>
);
You can use multiple variations for text lines, images, avatars — anything you need.
Step 3: Conditionally render based on loading state
function CardList({ loading, items }: { loading: boolean; items: any[] }) {
if (loading) {
return (
<div>
{Array.from({ length: 4 }).map((_, i) => (
<CardSkeleton key={i} />
))}
div>
);
}
return (
<div>
{items.map((item, i) => (
<Card key={i} data={item} />
))}
div>
);
}
✅ This gives users immediate feedback that content is loading — and the UI feels responsive.
⚙️ Why this approach is better than a spinner
- Gives visual context of what’s coming
- Reduces perceived loading time
- Matches the final layout, avoiding layout shift
- Can be customized to any shape: text, cards, images, etc.
🧠 Pro Tips
- Match skeleton size to final content size (avoid jumpy layout)
- Animate only when loading — avoid animating when content is ready
- Group skeletons into reusable components (e.g.,
)
🚀 Final Thoughts
Skeleton loaders are a great way to improve UX and make your app feel snappy — even when data takes time.
Using styled-components
makes it easy to style and reuse them across your app.
👉 Do you use skeleton loaders in your UI? Got a pattern you love? Drop it in the comments below! 👇
🔗 Follow me for more React + styled-components tips!