rflow: hidden;
}
.skeleton-shimmer {
background: linear-gradient(
90deg,
#f3f4f6 25%,
#e5e7eb 50%,
#f3f4f6 75%
);
background-size: 200% 100%;
animation: skeleton-slide 1.5s infinite linear;
}
@keyframes skeleton-slide {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
#### 2. React Component Design
The skeleton component should mirror the DOM structure of the loaded content. Inline styles for dimensions allow flexibility, while a wrapper component enforces the animation class.
```typescript
// components/Skeleton.tsx
import React from 'react';
import './styles/skeleton.css';
export interface SkeletonProps {
width?: string;
height?: string;
className?: string;
}
export const Skeleton: React.FC<SkeletonProps> = ({
width = '100%',
height = '1rem',
className = ''
}) => {
return (
<div
className={`skeleton-base skeleton-shimmer ${className}`}
style={{ width, height }}
aria-hidden="true"
/>
);
};
3. Layout Mirroring Strategy
Create a dedicated skeleton component for each content block. The skeleton must match the aspect ratios and spacing of the real component.
// components/ProductCardSkeleton.tsx
import { Skeleton } from './Skeleton';
export const ProductCardSkeleton = () => (
<article className="product-card">
<Skeleton width="100%" height="200px" className="mb-3" />
<Skeleton width="75%" height="1.25rem" className="mb-2" />
<Skeleton width="100%" height="0.875rem" className="mb-2" />
<div className="d-flex justify-content-between align-items-center">
<Skeleton width="3rem" height="1.5rem" />
<Skeleton width="4rem" height="1.5rem" />
</div>
</article>
);
4. Temporal Thresholding
Skeletons should only render if the load time exceeds a perceptual threshold. Rendering a skeleton for a fast response creates a "flash" effect that is more disruptive than a blank state. Implement a delay hook to manage this.
// hooks/useSkeletonDelay.ts
import { useState, useEffect } from 'react';
export const useSkeletonDelay = (isLoading: boolean, thresholdMs: number = 300) => {
const [showSkeleton, setShowSkeleton] = useState(false);
useEffect(() => {
if (!isLoading) {
setShowSkeleton(false);
return;
}
const timer = setTimeout(() => {
setShowSkeleton(true);
}, thresholdMs);
return () => clearTimeout(timer);
}, [isLoading, thresholdMs]);
return showSkeleton;
};
5. Integration
Combine the delay hook with the skeleton component to conditionally render the loading state.
// components/ProductList.tsx
import { useQuery } from '@tanstack/react-query';
import { useSkeletonDelay } from '../hooks/useSkeletonDelay';
import { ProductCardSkeleton } from './ProductCardSkeleton';
import { ProductCard } from './ProductCard';
export const ProductList = () => {
const { data, isLoading } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
});
const showSkeleton = useSkeletonDelay(isLoading);
if (showSkeleton) {
return (
<div className="grid">
{[1, 2, 3].map((i) => (
<ProductCardSkeleton key={i} />
))}
</div>
);
}
return (
<div className="grid">
{data?.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
};
Pitfall Guide
-
The Flash Artifact
- Explanation: Rendering a skeleton for requests that complete in under 300ms causes a visible flicker (blank β skeleton β content). This increases cognitive load and feels broken.
- Fix: Always implement a minimum display threshold (e.g., 300ms) before showing the skeleton. Use the
useSkeletonDelay pattern.
-
Layout Mismatch
- Explanation: If the skeleton dimensions or structure differ significantly from the loaded content, the brain cannot pattern-match. The transition feels jarring, and the perceived speed benefit is lost.
- Fix: Audit the loaded component's DOM and CSS. Replicate exact heights, widths, margins, and border radii in the skeleton.
-
Misapplication on Binary Actions
- Explanation: Using a skeleton for form submissions or button clicks is misleading. The user expects a success/error state, not a preview of the next page. A skeleton implies content is arriving, which may not be the case.
- Fix: Use spinners or button loading states for actions with binary outcomes or destructive operations.
-
Accessibility Neglect
- Explanation: Screen readers may announce skeleton elements as content, confusing users. Without proper attributes, skeletons are indistinguishable from real data to assistive technology.
- Fix: Add
aria-hidden="true" to skeleton elements. If the skeleton represents a live region, use role="status" and aria-busy="true" on the container.
-
Animation Jank
- Explanation: Animations that trigger layout or paint operations can cause frame drops, especially on low-end devices. This makes the application feel sluggish.
- Fix: Use
background-position or transform for animations. Avoid animating properties like width, height, or margin. Ensure animations run on the compositor thread.
-
Unpredictable Content Shapes
- Explanation: Skeletons fail when the content structure is unknown. For example, a search result that might return a single item or a table of fifty items cannot have a meaningful skeleton.
- Fix: Fall back to a spinner for queries with highly variable result shapes. Alternatively, use a generic list skeleton that implies "items are loading" without predicting specific layout.
-
Ignoring Network Conditions
- Explanation: Skeletons assume a steady stream of data. On flaky networks, skeletons may persist indefinitely, leading to user frustration if the request fails silently.
- Fix: Implement error boundaries and retry mechanisms. Ensure skeletons are replaced by error states when requests fail, not left hanging.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Content Fetch (>1s) | Skeleton Screen | Reduces anxiety via layout preview; leverages predictive processing. | Low (CSS/Component overhead) |
| Content Fetch (<300ms) | None | Skeleton causes flash; response feels instant without indicator. | None |
| Form Submission | Spinner | Binary outcome; skeleton implies content arrival which may not occur. | Low |
| Destructive Action | Spinner | Skeleton of deleted state is confusing; spinner confirms pending action. | Low |
| Dynamic/Unknown Shape | Spinner | Skeleton cannot predict layout; generic skeleton offers no cognitive benefit. | Low |
| Indeterminate Wait | Progress Bar | Skeleton implies structure; progress bar communicates duration/effort. | Medium |
Configuration Template
CSS Module:
/* skeleton.module.css */
.skeleton {
background-color: var(--skeleton-bg, #f3f4f6);
border-radius: var(--skeleton-radius, 0.375rem);
position: relative;
overflow: hidden;
}
.skeleton::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.6) 50%,
transparent 100%
);
animation: shimmer 1.5s infinite;
transform: translateX(-100%);
}
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}
React Hook:
// hooks/useLoadingDelay.ts
import { useState, useEffect } from 'react';
export const useLoadingDelay = (
isLoading: boolean,
delay: number = 300
): boolean => {
const [shouldShow, setShouldShow] = useState(false);
useEffect(() => {
if (!isLoading) {
setShouldShow(false);
return;
}
const timeout = setTimeout(() => setShouldShow(true), delay);
return () => clearTimeout(timeout);
}, [isLoading, delay]);
return shouldShow;
};
Quick Start Guide
- Define CSS Classes: Add the
.skeleton and .skeleton::after styles to your global stylesheet or CSS module. Configure CSS variables for background color and border radius to match your design system.
- Create Skeleton Component: Build a reusable
Skeleton component that accepts width, height, and className props. Apply the CSS classes and set aria-hidden="true".
- Implement Delay Hook: Create a
useLoadingDelay hook that returns true only after the specified delay (e.g., 300ms) if isLoading remains true.
- Mirror Layouts: For each content component, create a corresponding skeleton component that replicates the DOM structure and dimensions. Use the
Skeleton component for individual blocks.
- Integrate: In your parent component, use the delay hook to conditionally render the skeleton layout. Ensure spinners are used for actions and error states override skeletons on failure.