Skeletons
Skeletons are placeholder previews that indicate content is loading.
Basic Skeleton
<div style="width: 200px;">
<div class="Skeleton" style="height: 16px; width: 80%;"></div>
</div>
<div class="Skeleton" style="height: 16px; width: 80%;"></div>
Text Skeleton
Simulate loading text with multiple lines.
<div style="width: 300px; display: flex; flex-direction: column; gap: var(--space-2);">
<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>
</div>
<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 style="display: flex; gap: var(--space-4);">
<div class="Skeleton Skeleton--circle" style="width: 32px; height: 32px;"></div>
<div class="Skeleton Skeleton--circle" style="width: 48px; height: 48px;"></div>
<div class="Skeleton Skeleton--circle" style="width: 64px; height: 64px;"></div>
</div>
<div class="Skeleton Skeleton--circle" style="width: 48px; height: 48px;"></div>
Card Skeleton
<div class="Card" style="width: 280px;">
<div class="Skeleton" style="height: 140px; border-radius: 0;"></div>
<div class="Card-body">
<div class="Skeleton" style="height: 20px; width: 60%; margin-bottom: var(--space-2);"></div>
<div class="Skeleton" style="height: 14px; width: 100%; margin-bottom: var(--space-1);"></div>
<div class="Skeleton" style="height: 14px; width: 80%;"></div>
</div>
</div>
<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
<div style="width: 300px; display: flex; flex-direction: column; gap: var(--space-3);">
<div style="display: flex; gap: var(--space-3); align-items: center;">
<div class="Skeleton Skeleton--circle" style="width: 40px; height: 40px;"></div>
<div style="flex: 1; display: flex; flex-direction: column; gap: var(--space-1);">
<div class="Skeleton" style="height: 14px; width: 70%;"></div>
<div class="Skeleton" style="height: 12px; width: 50%;"></div>
</div>
</div>
<div style="display: flex; gap: var(--space-3); align-items: center;">
<div class="Skeleton Skeleton--circle" style="width: 40px; height: 40px;"></div>
<div style="flex: 1; display: flex; flex-direction: column; gap: var(--space-1);">
<div class="Skeleton" style="height: 14px; width: 60%;"></div>
<div class="Skeleton" style="height: 12px; width: 40%;"></div>
</div>
</div>
<div style="display: flex; gap: var(--space-3); align-items: center;">
<div class="Skeleton Skeleton--circle" style="width: 40px; height: 40px;"></div>
<div style="flex: 1; display: flex; flex-direction: column; gap: var(--space-1);">
<div class="Skeleton" style="height: 14px; width: 80%;"></div>
<div class="Skeleton" style="height: 12px; width: 55%;"></div>
</div>
</div>
</div>
Table Skeleton
<div style="width: 100%;">
<div style="display: flex; gap: var(--space-4); padding: var(--space-3) 0; border-bottom: 2px solid var(--bd);">
<div class="Skeleton" style="height: 12px; width: 80px;"></div>
<div class="Skeleton" style="height: 12px; width: 120px;"></div>
<div class="Skeleton" style="height: 12px; width: 60px;"></div>
</div>
<div style="display: flex; gap: var(--space-4); padding: var(--space-3) 0; border-bottom: 1px solid var(--bd);">
<div class="Skeleton" style="height: 14px; width: 80px;"></div>
<div class="Skeleton" style="height: 14px; width: 120px;"></div>
<div class="Skeleton" style="height: 14px; width: 60px;"></div>
</div>
<div style="display: flex; gap: var(--space-4); padding: var(--space-3) 0; border-bottom: 1px solid var(--bd);">
<div class="Skeleton" style="height: 14px; width: 80px;"></div>
<div class="Skeleton" style="height: 14px; width: 120px;"></div>
<div class="Skeleton" style="height: 14px; width: 60px;"></div>
</div>
</div>
Static Variant
Disable animation for reduced motion preferences.
<div class="Skeleton Skeleton--static" style="height: 16px; width: 200px;"></div>
<div class="Skeleton Skeleton--static">...</div>
Common Patterns
Profile Card Loading
<div style="max-width: 300px; padding: var(--space-4); border: 1px solid var(--bd); border-radius: var(--r-m);">
<div style="display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3);">
<div class="Skeleton Skeleton--circle" style="width: 48px; height: 48px;"></div>
<div style="flex: 1; display: flex; flex-direction: column; gap: var(--space-1);">
<div class="Skeleton" style="height: 16px; width: 70%;"></div>
<div class="Skeleton" style="height: 12px; width: 50%;"></div>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: var(--space-2);">
<div class="Skeleton" style="height: 14px; width: 100%;"></div>
<div class="Skeleton" style="height: 14px; width: 85%;"></div>
</div>
</div>
Article Feed
<div style="display: flex; flex-direction: column; gap: var(--space-4); max-width: 400px;">
<div style="display: flex; gap: var(--space-3);">
<div class="Skeleton" style="width: 100px; height: 80px; flex-shrink: 0;"></div>
<div style="flex: 1; display: flex; flex-direction: column; gap: var(--space-2);">
<div class="Skeleton" style="height: 16px; width: 90%;"></div>
<div class="Skeleton" style="height: 12px; width: 100%;"></div>
<div class="Skeleton" style="height: 12px; width: 60%;"></div>
</div>
</div>
<div style="display: flex; gap: var(--space-3);">
<div class="Skeleton" style="width: 100px; height: 80px; flex-shrink: 0;"></div>
<div style="flex: 1; display: flex; flex-direction: column; gap: var(--space-2);">
<div class="Skeleton" style="height: 16px; width: 75%;"></div>
<div class="Skeleton" style="height: 12px; width: 100%;"></div>
<div class="Skeleton" style="height: 12px; width: 45%;"></div>
</div>
</div>
</div>
Dashboard Widgets
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-3); width: 100%;">
<div style="padding: var(--space-3); background: var(--bg-s); border-radius: var(--r-m);">
<div class="Skeleton" style="height: 12px; width: 60%; margin-bottom: var(--space-2);"></div>
<div class="Skeleton" style="height: 24px; width: 40%;"></div>
</div>
<div style="padding: var(--space-3); background: var(--bg-s); border-radius: var(--r-m);">
<div class="Skeleton" style="height: 12px; width: 50%; margin-bottom: var(--space-2);"></div>
<div class="Skeleton" style="height: 24px; width: 35%;"></div>
</div>
<div style="padding: var(--space-3); background: var(--bg-s); border-radius: var(--r-m);">
<div class="Skeleton" style="height: 12px; width: 70%; margin-bottom: var(--space-2);"></div>
<div class="Skeleton" style="height: 24px; width: 45%;"></div>
</div>
</div>
Form Loading
<div style="max-width: 350px; display: flex; flex-direction: column; gap: var(--space-4);">
<div>
<div class="Skeleton" style="height: 12px; width: 80px; margin-bottom: var(--space-2);"></div>
<div class="Skeleton" style="height: 36px; width: 100%;"></div>
</div>
<div>
<div class="Skeleton" style="height: 12px; width: 100px; margin-bottom: var(--space-2);"></div>
<div class="Skeleton" style="height: 36px; width: 100%;"></div>
</div>
<div class="Skeleton" style="height: 36px; width: 120px;"></div>
</div>
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
| Class | Description |
|---|---|
.Skeleton | Base skeleton with shimmer animation |
.Skeleton--circle | Circular skeleton (for avatars) |
.Skeleton--static | No animation (reduced motion) |
Attributes
| Attribute | Description |
|---|---|
aria-busy="true" | On the container to indicate loading state |
aria-label | On 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 containersaria-labelto describe the loading content- Remove
aria-busywhen 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