Skip to main content

Last updated: March 13, 2026

How to Create CSS Skeleton Loaders

The complete guide to building beautiful skeleton loading screens. Learn shimmer, pulse, and wave animations with accessible, production-ready code.

What Are Skeleton Loaders?

Skeleton loaders are placeholder UI elements that mimic the shape of your content while it loads. Instead of showing a blank page or generic spinner, skeleton screens give users a preview of the page structure, improving perceived performance and reducing frustration.

Skeleton Loaders

  • + Show content structure preview
  • + Reduce perceived loading time
  • + Prevent layout shifts
  • + Feel more polished and intentional

Traditional Spinners

  • - No indication of content type
  • - Feel slower (same duration feels longer)
  • - Cause layout shifts when content loads
  • - Generic and impersonal

Popular apps like Facebook, YouTube, LinkedIn, and Slack all use skeleton loaders for a smoother user experience.

1

Basic Skeleton Structure

Simple gray placeholder elements that match the content dimensions. No animation - just the foundation.

Live Preview

HTML + CSS Code

<!-- HTML -->
<div class="skeleton-container">
  <div class="skeleton skeleton-text"></div>
  <div class="skeleton skeleton-text short"></div>
  <div class="skeleton skeleton-text"></div>
</div>
/* CSS */
.skeleton {
  background: #e0e0e0;
  border-radius: 4px;
}

.skeleton-text {
  height: 16px;
  margin-bottom: 12px;
}

.skeleton-text.short {
  width: 60%;
}
2

Shimmer/Wave Effect

A moving gradient that creates a shimmer or wave effect, giving the impression of loading activity.

Live Preview

HTML + CSS Code

<!-- HTML -->
<div class="skeleton-shimmer">
  <div class="skeleton skeleton-text"></div>
  <div class="skeleton skeleton-text short"></div>
</div>
/* CSS */
.skeleton {
  background: linear-gradient(
    90deg,
    #e0e0e0 25%,
    #f0f0f0 50%,
    #e0e0e0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
}

.skeleton-text {
  height: 16px;
  margin-bottom: 12px;
}

.skeleton-text.short {
  width: 60%;
}

@keyframes shimmer {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}
3

Pulse/Fade Effect

A simple opacity animation that pulses between lighter and darker shades, indicating loading.

Live Preview

HTML + CSS Code

<!-- HTML -->
<div class="skeleton-pulse">
  <div class="skeleton skeleton-text"></div>
  <div class="skeleton skeleton-text short"></div>
</div>
/* CSS */
.skeleton {
  background: #e0e0e0;
  border-radius: 4px;
  animation: pulse 1.5s ease-in-out infinite;
}

.skeleton-text {
  height: 16px;
  margin-bottom: 12px;
}

.skeleton-text.short {
  width: 60%;
}

@keyframes pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}
4

Avatar/Circle Skeleton

Circular skeleton for profile pictures and avatar placeholders.

Live Preview

HTML + CSS Code

<!-- HTML -->
<div class="skeleton-avatar-group">
  <div class="skeleton skeleton-avatar"></div>
  <div class="skeleton-content">
    <div class="skeleton skeleton-text name"></div>
    <div class="skeleton skeleton-text subtitle"></div>
  </div>
</div>
/* CSS */
.skeleton-avatar-group {
  display: flex;
  align-items: center;
  gap: 12px;
}

.skeleton-avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: linear-gradient(
    90deg,
    #e0e0e0 25%,
    #f0f0f0 50%,
    #e0e0e0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

.skeleton-content {
  flex: 1;
}

.skeleton-text {
  background: linear-gradient(
    90deg,
    #e0e0e0 25%,
    #f0f0f0 50%,
    #e0e0e0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
  height: 14px;
  margin-bottom: 8px;
}

.skeleton-text.name {
  width: 120px;
}

.skeleton-text.subtitle {
  width: 80px;
  margin-bottom: 0;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
5

Image Placeholder Skeleton

Rectangular skeleton for image placeholders with aspect ratio preservation.

Live Preview

HTML + CSS Code

<!-- HTML -->
<div class="skeleton skeleton-image">
  <svg class="image-icon" viewBox="0 0 24 24">
    <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
  </svg>
</div>
/* CSS */
.skeleton-image {
  width: 100%;
  aspect-ratio: 16 / 9;
  background: linear-gradient(
    90deg,
    #e0e0e0 25%,
    #f0f0f0 50%,
    #e0e0e0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.image-icon {
  width: 48px;
  height: 48px;
  fill: #c0c0c0;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
6

Article Card Skeleton

Complete article card skeleton with image, title, description, and metadata.

Live Preview

HTML + CSS Code

<!-- HTML -->
<article class="article-skeleton" aria-busy="true" role="article">
  <div class="skeleton skeleton-image"></div>
  <div class="article-content">
    <div class="skeleton skeleton-title"></div>
    <div class="skeleton skeleton-text"></div>
    <div class="skeleton skeleton-text short"></div>
    <div class="skeleton-meta">
      <div class="skeleton skeleton-avatar-small"></div>
      <div class="skeleton skeleton-text meta"></div>
    </div>
  </div>
</article>
/* CSS */
.article-skeleton {
  background: #fff;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.skeleton {
  background: linear-gradient(
    90deg,
    #e0e0e0 25%,
    #f0f0f0 50%,
    #e0e0e0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

.skeleton-image {
  width: 100%;
  height: 180px;
}

.article-content {
  padding: 16px;
}

.skeleton-title {
  height: 24px;
  border-radius: 4px;
  margin-bottom: 12px;
  width: 80%;
}

.skeleton-text {
  height: 14px;
  border-radius: 4px;
  margin-bottom: 8px;
}

.skeleton-text.short {
  width: 60%;
}

.skeleton-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 16px;
}

.skeleton-avatar-small {
  width: 32px;
  height: 32px;
  border-radius: 50%;
}

.skeleton-text.meta {
  width: 100px;
  margin-bottom: 0;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
7

Profile Card Skeleton

Profile card with centered avatar, name, bio, and action buttons.

Live Preview

HTML + CSS Code

<!-- HTML -->
<div class="profile-skeleton" aria-busy="true">
  <div class="skeleton skeleton-avatar-large"></div>
  <div class="skeleton skeleton-name"></div>
  <div class="skeleton skeleton-bio"></div>
  <div class="skeleton skeleton-bio short"></div>
  <div class="skeleton-buttons">
    <div class="skeleton skeleton-button"></div>
    <div class="skeleton skeleton-button secondary"></div>
  </div>
</div>
/* CSS */
.profile-skeleton {
  background: #fff;
  border-radius: 16px;
  padding: 24px;
  text-align: center;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.skeleton {
  background: linear-gradient(
    90deg,
    #e0e0e0 25%,
    #f0f0f0 50%,
    #e0e0e0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

.skeleton-avatar-large {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  margin: 0 auto 16px;
}

.skeleton-name {
  height: 24px;
  width: 150px;
  border-radius: 4px;
  margin: 0 auto 12px;
}

.skeleton-bio {
  height: 14px;
  width: 200px;
  border-radius: 4px;
  margin: 0 auto 8px;
}

.skeleton-bio.short {
  width: 140px;
}

.skeleton-buttons {
  display: flex;
  justify-content: center;
  gap: 12px;
  margin-top: 20px;
}

.skeleton-button {
  height: 40px;
  width: 100px;
  border-radius: 8px;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
8

List Item Skeleton

Repeatable list item skeleton for feeds, comments, or notification lists.

Live Preview

HTML + CSS Code

<!-- HTML -->
<ul class="list-skeleton" aria-busy="true" role="list">
  <li class="list-item-skeleton">
    <div class="skeleton skeleton-avatar"></div>
    <div class="list-item-content">
      <div class="skeleton skeleton-text title"></div>
      <div class="skeleton skeleton-text subtitle"></div>
    </div>
    <div class="skeleton skeleton-action"></div>
  </li>
  <!-- Repeat for more items -->
</ul>
/* CSS */
.list-skeleton {
  list-style: none;
  padding: 0;
  margin: 0;
}

.list-item-skeleton {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border-bottom: 1px solid #eee;
}

.skeleton {
  background: linear-gradient(
    90deg,
    #e0e0e0 25%,
    #f0f0f0 50%,
    #e0e0e0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

.skeleton-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  flex-shrink: 0;
}

.list-item-content {
  flex: 1;
  min-width: 0;
}

.skeleton-text {
  border-radius: 4px;
}

.skeleton-text.title {
  height: 16px;
  width: 60%;
  margin-bottom: 6px;
}

.skeleton-text.subtitle {
  height: 12px;
  width: 40%;
}

.skeleton-action {
  width: 24px;
  height: 24px;
  border-radius: 4px;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
9

Table Row Skeleton

Table skeleton for data grids and tabular content loading states.

Live Preview

HTML + CSS Code

<!-- HTML -->
<table class="table-skeleton" aria-busy="true">
  <thead>
    <tr>
      <th><div class="skeleton skeleton-header"></div></th>
      <th><div class="skeleton skeleton-header"></div></th>
      <th><div class="skeleton skeleton-header"></div></th>
      <th><div class="skeleton skeleton-header"></div></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><div class="skeleton skeleton-cell"></div></td>
      <td><div class="skeleton skeleton-cell"></div></td>
      <td><div class="skeleton skeleton-cell"></div></td>
      <td><div class="skeleton skeleton-cell short"></div></td>
    </tr>
    <!-- Repeat for more rows -->
  </tbody>
</table>
/* CSS */
.table-skeleton {
  width: 100%;
  border-collapse: collapse;
}

.table-skeleton th,
.table-skeleton td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #eee;
}

.skeleton {
  background: linear-gradient(
    90deg,
    #e0e0e0 25%,
    #f0f0f0 50%,
    #e0e0e0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
}

.skeleton-header {
  height: 14px;
  width: 80px;
}

.skeleton-cell {
  height: 16px;
  width: 100%;
}

.skeleton-cell.short {
  width: 60px;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
10

Accessible Skeleton (Best Practice)

Production-ready skeleton with proper accessibility attributes and reduced motion support.

Live Preview

Loading...

HTML + CSS Code

<!-- HTML -->
<div
  class="skeleton-container"
  aria-busy="true"
  aria-label="Loading content"
  role="status"
>
  <div class="skeleton skeleton-image"></div>
  <div class="skeleton skeleton-text"></div>
  <div class="skeleton skeleton-text short"></div>
  <span class="sr-only">Loading...</span>
</div>
/* CSS */
/* Screen reader only class */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skeleton-container {
  padding: 16px;
}

.skeleton {
  background: linear-gradient(
    90deg,
    #e0e0e0 25%,
    #f0f0f0 50%,
    #e0e0e0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
}

.skeleton-image {
  width: 100%;
  height: 200px;
  margin-bottom: 16px;
}

.skeleton-text {
  height: 16px;
  margin-bottom: 12px;
}

.skeleton-text.short {
  width: 60%;
}

/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: pulse 2s ease-in-out infinite;
  }
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

Best Practices

1. Match Content Dimensions

Design skeletons to match the actual content size as closely as possible. This prevents jarring layout shifts when content loads.

/* Match your actual image container */
.skeleton-image {
  width: 100%;
  aspect-ratio: 16 / 9;
}

2. Use Semantic ARIA

Add accessibility attributes so screen readers understand the loading state and can announce it to users.

<div
  aria-busy="true"
  role="status"
  aria-label="Loading content"
>
  <span class="sr-only">Loading...</span>
</div>

3. Respect Reduced Motion

Some users prefer reduced motion. Use CSS media queries to provide a simpler animation alternative.

@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: pulse 2s ease-in-out infinite;
  }
}

4. Keep Animations Subtle

Animation should be smooth and not distracting. Use slow durations (1.5-2s) and subtle color changes.

/* Subtle shimmer colors */
background: linear-gradient(
  90deg,
  #e0e0e0 25%,  /* base */
  #f0f0f0 50%,  /* highlight */
  #e0e0e0 75%   /* base */
);

Frequently Asked Questions

What is a skeleton loader?

A skeleton loader (also called a skeleton screen or content placeholder) is a UI pattern that shows a simplified preview of a page's layout while content is loading. Instead of a blank page or spinner, users see gray shapes that mimic the structure of the actual content, providing a better perceived performance experience.

Why use skeleton loaders instead of spinners?

Skeleton loaders provide better UX because they: 1) Show users what type of content is coming (setting expectations), 2) Feel faster than spinners due to perceived performance, 3) Reduce layout shift when content loads, and 4) Make the loading state feel more intentional and polished. However, spinners are still useful for quick actions or when content structure is unknown.

Should I use shimmer or pulse animation?

Shimmer (wave) animations are generally preferred because they convey more activity and feel faster. Use pulse animations when: 1) You want a subtler effect, 2) The skeleton is small or simple, or 3) You're concerned about motion sensitivity. Always respect prefers-reduced-motion for accessibility.

How do I make skeleton loaders accessible?

To make skeletons accessible: 1) Add aria-busy='true' to the loading container, 2) Use role='status' to announce loading state, 3) Include hidden text like <span class='sr-only'>Loading...</span> for screen readers, 4) Use @media (prefers-reduced-motion: reduce) to provide a simpler animation for motion-sensitive users.

Should skeletons match the exact content dimensions?

Yes, ideally! Matching dimensions prevents layout shift when content loads, creating a smooth transition. If exact dimensions aren't possible, use reasonable estimates based on typical content. For dynamic content like text, show 2-3 lines of skeleton text at average width.