SVG Placeholders and Skeleton Screens

May 28, 20266 min read

A blank screen while data loads feels slow. A skeleton screen — the gray placeholder shapes that mimic the layout of incoming content — makes the same wait feel faster because users see immediate visual progress. SVG is the ideal format for these placeholders: it is resolution-independent, tiny in file size, and animatable with pure CSS. This guide covers building effective skeleton screens with SVG components.

Why SVG Beats Other Placeholder Formats

FormatFile Size (typical card)Resolution IndependenceAnimatableAccessibility
PNG5–20 KBNoNoDecorative alt
Base64 CSS2–8 KBNoNoDecorative alt
CSS gradients~200 BYesLimitedDecorative
SVG inline~300 BYesYesFull ARIA

SVG wins because it combines the visual precision of a real image layout with the file size of a few CSS properties. A card skeleton with avatar, title, and description lines is roughly 300 bytes as inline SVG versus several kilobytes for any raster approach.

Building a Card Skeleton

Here is a complete skeleton screen for a content card with an avatar, heading, and two lines of body text:

<div class="skeleton-card" role="status" aria-label="Loading content">
  <svg
    viewBox="0 0 400 200"
    xmlns="http://www.w3.org/2000/svg"
    aria-hidden="true"
    class="skeleton-svg"
  >
    <!-- Avatar circle -->
    <circle cx="40" cy="40" r="24" fill="#e0e0e0" rx="4" />

    <!-- Title line -->
    <rect x="76" y="24" width="200" height="14" rx="4" fill="#e0e0e0" />

    <!-- Subtitle line -->
    <rect x="76" y="46" width="140" height="10" rx="3" fill="#e0e0e0" />

    <!-- Body line 1 -->
    <rect x="16" y="88" width="368" height="10" rx="3" fill="#e0e0e0" />

    <!-- Body line 2 -->
    <rect x="16" y="108" width="280" height="10" rx="3" fill="#e0e0e0" />

    <!-- Image placeholder -->
    <rect x="16" y="136" width="368" height="48" rx="6" fill="#e0e0e0" />
  </svg>
</div>

Notice the role="status" and aria-label on the wrapper. Screen readers announce "Loading content" instead of ignoring the skeleton entirely. The SVG itself uses aria-hidden="true" because its visual elements are decorative — the meaningful information is in the wrapper's ARIA attributes.

Adding the Shimmer Animation

The pulsing shimmer effect — the signature visual of skeleton screens — is a simple CSS gradient animation applied to the SVG shapes:

.skeleton-card {
  --shimmer-color: #e0e0e0;
  --shimmer-highlight: #f5f5f5;
  --shimmer-speed: 1.5s;
}

.skeleton-svg rect,
.skeleton-svg circle {
  animation: shimmer var(--shimmer-speed) ease-in-out infinite;
}

@keyframes shimmer {
  0% { fill: var(--shimmer-color); }
  50% { fill: var(--shimmer-highlight); }
  80% { fill: var(--shimmer-color); }
  100% { fill: var(--shimmer-color); }
}

/* Dark mode support */
@media (prefers-color-scheme: dark) {
  .skeleton-card {
    --shimmer-color: #333333;
    --shimmer-highlight: #555555;
  }
}

An alternative shimmer technique uses a moving gradient overlay instead of animating the fill directly. This creates a more polished sweeping highlight effect:

.skeleton-card {
  position: relative;
  overflow: hidden;
}

.skeleton-card::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(255, 255, 255, 0.3) 50%,
    transparent 100%
  );
  animation: sweep 1.5s ease-in-out infinite;
}

@keyframes sweep {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

The sweep approach avoids repaints on individual SVG elements — the browser composites a single pseudo-element layer, which is GPU-accelerated and smoother.

Responsive Skeleton Layouts

SVG skeletons need to scale with the container. Use viewBox with percentage-based dimensions:

.skeleton-svg {
  width: 100%;
  height: auto;
  display: block;
}

The viewBox="0 0 400 200" definition ensures the shapes maintain their relative positions regardless of the container width. This works with Flexbox and CSS Grid layouts where the card width is fluid.

For layouts with varying numbers of skeleton cards (e.g., a grid that shows 1 column on mobile, 3 on desktop), generate the appropriate number of skeleton elements and let CSS grid handle the placement — same as you would with real content cards.

Transitioning from Skeleton to Content

The moment data arrives, swap the skeleton for real content with a fade transition to avoid a jarring flash:

.content-card {
  opacity: 1;
  transition: opacity 0.3s ease;
}

.content-card.loading {
  opacity: 0;
  position: absolute;
  pointer-events: none;
}

.skeleton-card.hidden {
  opacity: 0;
  transition: opacity 0.2s ease;
}
function showContent(cardElement, contentHtml) {
  const skeleton = cardElement.querySelector('.skeleton-card')
  const content = cardElement.querySelector('.content-card')

  content.innerHTML = contentHtml
  skeleton.classList.add('hidden')

  setTimeout(() => {
    skeleton.remove()
    content.classList.remove('loading')
  }, 200)
}

The skeleton fades out over 200ms while the content fades in over 300ms. Removing the skeleton DOM element after the transition prevents it from consuming memory on long-scrolling pages.

Generate SVG placeholder images for your skeleton components at /tools/placeholder-image, which produces resolution-independent SVG output with customizable dimensions and colors.