Skeletons

Skeletons are placeholder previews that indicate content is loading.

Basic Skeleton

<div class="Skeleton" style="height: 16px; width: 80%;"></div>

Text Skeleton

Simulate loading text with multiple lines.

<div class="Skeleton" style="height: 16px; width: 100%;"></div>
<div class="Skeleton" style="height: 16px; width: 90%;"></div>
<div class="Skeleton" style="height: 16px; width: 75%;"></div>

Avatar Skeleton

<div class="Skeleton Skeleton--circle" style="width: 48px; height: 48px;"></div>

Card Skeleton

<div class="Card">
    <div class="Skeleton" style="height: 140px;"></div>
    <div class="Card-body">
        <div class="Skeleton" style="height: 20px; width: 60%;"></div>
        <div class="Skeleton" style="height: 14px; width: 100%;"></div>
        <div class="Skeleton" style="height: 14px; width: 80%;"></div>
    </div>
</div>

List Skeleton

Table Skeleton

Static Variant

Disable animation for reduced motion preferences.

<div class="Skeleton Skeleton--static">...</div>

Common Patterns

Profile Card Loading

Article Feed

Dashboard Widgets

Form Loading


Customization

Override skeleton styling with CSS custom properties:

/* Custom shimmer colors */
.Skeleton {
  --skeleton-bg: oklch(90% 0 0);
  --skeleton-shimmer: oklch(95% 0 0);
}

Pulse Animation (instead of shimmer)

.Skeleton--pulse {
  animation: skeleton-pulse 1.5s ease-in-out infinite;
}

@keyframes skeleton-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

Theming

[data-theme="dark"] .Skeleton {
  background: oklch(25% 0 0);
}

[data-theme="dark"] .Skeleton::after {
  background: linear-gradient(
    90deg,
    transparent,
    oklch(30% 0 0),
    transparent
  );
}

API Reference

ClassDescription
.SkeletonBase skeleton with shimmer animation
.Skeleton--circleCircular skeleton (for avatars)
.Skeleton--staticNo animation (reduced motion)

Attributes

AttributeDescription
aria-busy="true"On the container to indicate loading state
aria-labelOn the container to describe what's loading

Sizing

Skeletons use inline style attributes for dimensions. Set height to match the text line-height and width as percentage or fixed value.


CSS Reference

/* Base skeleton */
.Skeleton {
  background: var(--bg-s);
  border-radius: var(--r-s);
  position: relative;
  overflow: hidden;
}

/* Shimmer animation */
.Skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent,
    oklch(100% 0 0 / 0.08),
    transparent
  );
  animation: skeleton-shimmer 1.5s ease-in-out infinite;
}

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

/* Circle variant */
.Skeleton--circle {
  border-radius: 50%;
}

/* Static variant */
.Skeleton--static::after {
  animation: none;
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  .Skeleton::after {
    animation: none;
  }
}

Accessibility

Screen Reader Guidance

Use aria-busy="true" on the container while content is loading. Add aria-label to describe what’s being loaded. When content finishes loading, remove aria-busy or set it to false.

<div aria-busy="true" aria-label="Loading user profile">
    <div class="Skeleton" style="height: 16px; width: 80%;"></div>
    <div class="Skeleton" style="height: 16px; width: 60%;"></div>
</div>

Reduced Motion

Respect prefers-reduced-motion by disabling the shimmer animation. Use .Skeleton--static as a manual override, or rely on the CSS media query which handles it automatically.

ARIA Attributes

  • aria-busy="true" on loading containers
  • aria-label to describe the loading content
  • Remove aria-busy when content loads
  • Use aria-live="polite" on the container if content loads asynchronously

Best Practices

Do

  • Match the final content layout — Skeleton shape should mirror the real content
  • Vary line widths — Different widths (100%, 80%, 60%) look more natural than uniform blocks
  • Respect reduced motion — Disable shimmer for users who prefer reduced motion
  • Group related skeletons — Wrap in a container with aria-busy="true"
  • Transition smoothly — Fade skeletons into real content rather than hard-swapping
  • Use for layout-heavy content — Cards, lists, and dashboards benefit most from skeletons

Don’t

  • Use for fast loads — Content appearing in under 300ms doesn’t need a skeleton
  • Animate everything — Overuse of shimmer is distracting; use static for secondary areas
  • Mismatch dimensions — A skeleton taller than the real content causes layout shift
  • Use skeletons for errors — Show error states explicitly, not loading placeholders
  • Nest skeletons deeply — Keep the placeholder structure simple and flat
  • Forget to remove aria-busy — Stale aria-busy="true" confuses screen readers