Skeleton
Skeleton screens replace content with animated placeholder shapes during loading. They reduce perceived wait time and prevent layout shift. Use the getSkeletonStyle() utility to generate theme-aware skeleton styles, or apply the CSS class directly.
Example
Skeleton placeholders in both light and dark themes. The shimmer runs at 2 s to feel natural — not rushed.
Skeleton Light
Skeleton Dark
Usage
Skeleton styles can illustrate the loading state of complex elements such as cards, lists, or entire page sections.
Do
- ✓Match the skeleton shape closely to the real content — use a box for images, short lines for headings, and longer lines for body text.
- ✓Apply aria-busy="true" and a descriptive aria-label to the skeleton container so screen readers announce the loading state.
- ✓Remove skeleton nodes entirely and replace them with real content once loading is complete.
- ✓Respect reduced-motion: disable the shimmer animation when prefers-reduced-motion: reduce is set.
Don't
- ✕Don't use skeleton screens for operations faster than 300 ms — a brief delay feels like a flash, not a loading state.
- ✕Don't leave skeletons visible indefinitely — always show an error or empty state if loading fails.
- ✕Don't animate multiple skeletons at different speeds; stagger delays (0.1 s increments) but keep the duration consistent at 2 s.
- ✕Don't use skeletons for simple single-value fields — a spinner or inline placeholder is more appropriate.
Animation speed: The shimmer runs at 2 s — matching Diwa guidance. Faster animations (under 1.5 s) feel jittery and anxious; slower ones (over 2.5 s) feel stalled. Do not override animation-duration on individual skeletons.
Styles
Use the JS utility for CSS-in-JS or inline styles. Use the CSS class directly for stylesheet-based projects. Always include the reduced-motion reset.
// JS
import { getSkeletonStyle } from '@diwacopilot/components/styles';
// Returns a CSS-in-JS object with the shimmer gradient + animation
const lightSkeleton = getSkeletonStyle({ theme: 'light' });
const darkSkeleton = getSkeletonStyle({ theme: 'dark' });
// Usage in React
<div style={{ ...lightSkeleton, height: '160px', borderRadius: '8px' }} />
<div style={{ ...lightSkeleton, height: '14px', width: '60%' }} />
/* ─── CSS ─────────────────────────────────────────────────────────────── */
@keyframes diwa-skeleton-pulse {
0% { background-position: -200% center; }
100% { background-position: 200% center; }
}
/* Light theme */
.skeleton--light {
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0.04) 25%,
rgba(0, 0, 0, 0.10) 50%,
rgba(0, 0, 0, 0.04) 75%
);
background-size: 200% 100%;
animation: diwa-skeleton-pulse 2s ease-in-out infinite;
border-radius: 4px;
}
/* Dark theme */
.skeleton--dark {
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.04) 25%,
rgba(255, 255, 255, 0.12) 50%,
rgba(255, 255, 255, 0.04) 75%
);
background-size: 200% 100%;
animation: diwa-skeleton-pulse 2s ease-in-out infinite;
border-radius: 4px;
}
/* Stagger delay for multiple lines */
.skeleton:nth-child(2) { animation-delay: 0.1s; }
.skeleton:nth-child(3) { animation-delay: 0.2s; }
/* Accessibility — reduced motion */
@media (prefers-reduced-motion: reduce) {
.skeleton--light,
.skeleton--dark {
animation: none;
background: rgba(0, 0, 0, 0.06);
}
}
/* Accessibility — ARIA */
<div aria-busy="true" aria-label="Loading content">
<div class="skeleton--light" style="height:14px; width:60%" />
<div class="skeleton--light" style="height:14px; width:85%" />
</div>@use '@diwacopilot/components/styles' as *;
@keyframes diwa-skeleton-pulse {
0% { background-position: -200% center; }
100% { background-position: 200% center; }
}
.skeleton {
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.04) 25%,
rgba(255, 255, 255, 0.12) 50%,
rgba(255, 255, 255, 0.04) 75%
);
background-size: 200% 100%;
animation: diwa-skeleton-pulse 2s ease-in-out infinite;
border-radius: $diwa-border-radius-sm;
}
@media (prefers-reduced-motion: reduce) {
.skeleton { animation: none; }
}