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!