Browse by section

Web Design 日本語

Build a Skeleton Loader in CSS

This article covers what a skeleton loader is and how to build one.

The short answer: grey blocks with a light band sweeping across them, hidden by swapping a class once loading finishes.

The difference from a spinner is that a skeleton shows what is about to appear. Because it holds the shape of the layout, the page also jumps around less when the content lands.

Sponsored

What a skeleton loader is

Without any loading state, a page shows either nothing or a blank space while it waits. The user has no way to tell whether anything is happening.

A skeleton fills that gap with a visual placeholder, communicating that the content is on its way and roughly what shape it will take.

Building one

The HTML structure

Mirror the structure of the real content. Use elements that correspond to the blocks that will replace them.

<div class="skeleton-wrap">
  <div class="skeleton-loader skeleton-title"></div>
  <div class="skeleton-loader skeleton-text"></div>
  <div class="skeleton-loader skeleton-text"></div>
</div>

The CSS

.skeleton-loader {
  /* a grey base with a lighter band over it */
  background-color: #eee;
  background-image: linear-gradient(
    90deg,
    #eee 0%,
    #f5f5f5 50%,
    #eee 100%
  );
  background-size: 200% 100%;
  animation: skeleton-loading 1.2s infinite linear;
  border-radius: 4px;
}

@keyframes skeleton-loading {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

/* used to hide it once loading completes */
.skeleton-loader.hidden {
  display: none;
}

A sweeping band reads as “in progress” far more clearly than a static block. The gradient is laid out at twice the element’s width and moved with background-position.

background-position is animatable, which is what makes this work. background-image itself is not, so swapping between images is not an option here.

Size the blocks to match the content they stand in for:

.skeleton-title {
  height: 24px;
  width: 60%;
  margin-bottom: 12px;
}

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

.skeleton-text:last-child {
  width: 80%;  /* a shorter final line looks more natural */
}

Honour reduced-motion preferences. A continuously looping animation is exactly the kind of thing that setting exists for.

@media (prefers-reduced-motion: reduce) {
  .skeleton-loader {
    animation: none;
  }
}

The grey blocks remain visible with the animation off, so the loading state still reads.

Hiding it with JavaScript

window.addEventListener('load', function() {
  document.querySelector('.skeleton-loader').classList.add('hidden');
});

You have to define the .hidden class yourself — that is the display: none rule in the CSS above. Adding a class from JavaScript does nothing on its own.

For data loaded from an API, tie it to the fetch rather than the load event. load fires when the page’s own resources finish, which is a different moment entirely.

async function loadContent() {
  const skeleton = document.querySelector('.skeleton-loader');
  const content = document.querySelector('.content');

  try {
    const res = await fetch('/api/items');
    const data = await res.json();
    render(data);
  } finally {
    // clear the skeleton whether it succeeded or failed
    skeleton.classList.add('hidden');
    content.classList.remove('hidden');
  }
}

Removing it in finally matters. Hide it only on success and a failed request leaves the skeleton animating forever.

Sponsored

Things to watch for

A skeleton that does not match the real layout is worse than none.

The skeleton is a promise about what is coming. If the sizes and positions differ from the content that arrives, the page shifts at the exact moment it was meant to feel settled.

For very fast loads, consider not showing it at all. A skeleton that appears and vanishes within 100ms just reads as a flash. Delaying it avoids that:

// only show it if loading takes more than 300ms
const timer = setTimeout(() => skeleton.classList.remove('hidden'), 300);
// on completion
clearTimeout(timer);

Summary

A skeleton loader is a grey placeholder with a moving highlight, removed by a class swap. The details that matter are matching the real layout, clearing it in finally, and not showing it for loads that finish instantly.

The same class-swapping pattern drives building an accordion menu. To run code when a class changes, see detecting class changes with MutationObserver.