name: content-matched-shimmer description: >- Standard operating procedure and architectural patterns for designing bespoke, content-matched shimmer skeletons for websites and admin panels, eliminating mock/dummy fallbacks, preventing Cumulative Layout Shift (CLS), and enforcing motion accessibility.
Content-Matched Shimmer & Zero-Mock Loading Architecture
This skill defines the end-to-end design patterns, CSS implementations, component primitives, and state handling rules for implementing high-fidelity shimmer skeleton screens across web applications and admin panels.
1. Core Principles
A. The 1:1 Content-Matched Parity Principle
A skeleton loader must not be a generic grey box, a random cluster of bars, or a centered spinning icon. It must mirror the exact layout, geometry, typography, and spacing of the rendered page:
- Card Grids: Skeleton cards must share identical padding, borders, corner radii, and responsive grid breakpoints (
grid-cols-1 md:grid-cols-2 lg:grid-cols-3). - Data Tables: Skeleton tables must match column counts, header heights, row spacing, and cell alignments (e.g. numeric columns right-aligned).
- Detail Pages: Match page headers, action button widths, status badge pills, tab navigation bars, and multi-column body layouts.
- Zero CLS (Cumulative Layout Shift): When data finishes loading, DOM elements must cleanly swap in place without pushing surrounding content up or down.
B. The Zero-Mock-Data Rule
Never leave hardcoded dummy arrays, mock objects, or sample items in production code to "simulate" populated content or mask empty/loading states:
- Loading State: Initial state is
loading = true(or an active query state); render the content-matched shimmer. - Empty State: If the fetch returns
[]ornull, render a deliberate empty state UI with clear guidance or actions, not fallback mock data. - Error State: Handle network/server failures explicitly with retry mechanisms or graceful error alerts, never silently returning dummy data.
C. Motion Accessibility (prefers-reduced-motion)
Shimmer animations rely on continuous CSS gradient transforms. For users with vestibular motion disorders or accessibility preferences, infinite loop animations must be cleanly disabled via @media (prefers-reduced-motion: reduce).
2. CSS Shimmer Engine
Implement the shimmer beam using a GPU-accelerated CSS pseudo-element (::after) with translateX animation.
/* globals.css */
@keyframes shimmer-slide {
100% {
transform: translateX(100%);
}
}
.shimmer {
position: relative;
overflow: hidden;
background-color: rgba(0, 0, 0, 0.05); /* Adapt to theme background */
}
.shimmer::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: translateX(-100%);
background-image: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.65) 50%,
transparent 100%
);
animation: shimmer-slide 1.8s infinite;
content: "";
pointer-events: none;
}
/* Dark mode support */
.dark .shimmer {
background-color: rgba(255, 255, 255, 0.06);
}
.dark .shimmer::after {
background-image: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.08) 50%,
transparent 100%
);
}
/* Accessibility: respect reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
.shimmer::after {
animation: none;
display: none;
}
}
3. Atomic Skeleton Primitives
Build low-level building blocks that encapsulate consistent border radius, sizing, and theme classes.
// components/shimmer/Shimmer.tsx
import React from "react";
interface ShimmerProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
}
export function Shimmer({ className = "", ...props }: ShimmerProps) {
return <div className={`shimmer rounded-md ${className}`} {...props} />;
}
export function ShimmerText({
className = "",
width = "w-full",
height = "h-4",
}: {
className?: string;
width?: string;
height?: string;
}) {
return <Shimmer className={`${width} ${height} rounded ${className}`} />;
}
export function ShimmerCircle({
size = "w-10 h-10",
className = "",
}: {
size?: string;
className?: string;
}) {
return <Shimmer className={`${size} rounded-full shrink-0 ${className}`} />;
}
4. Bespoke Skeleton Patterns
Pattern 1: Admin KPI / Metric Stat Cards
Used for dashboard stat summaries, analytics counters, and financial widgets.
export function StatCardsSkeleton({ count = 4 }: { count?: number }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{Array.from({ length: count }).map((_, idx) => (
<div
key={idx}
className="bg-white rounded-xl p-5 border border-slate-200/80 shadow-sm space-y-3"
>
<div className="flex items-center justify-between">
<ShimmerText height="h-3.5" width="w-24" />
<Shimmer className="w-8 h-8 rounded-lg" />
</div>
<ShimmerText height="h-7" width="w-32" />
<div className="flex items-center gap-2 pt-1">
<Shimmer className="h-4 w-12 rounded" />
<ShimmerText height="h-3" width="w-20" />
</div>
</div>
))}
</div>
);
}
Pattern 2: Admin Data Table
Used for listing pages (users, orders, submissions, transactions). Mirrors real columns, headers, and action button dimensions.
export function TableSkeleton({
rows = 6,
columns = 5,
}: {
rows?: number;
columns?: number;
}) {
return (
<div className="bg-white rounded-xl border border-slate-200/80 shadow-sm overflow-hidden">
{/* Table Controls / Search Bar */}
<div className="p-4 border-b border-slate-100 flex items-center justify-between gap-4">
<Shimmer className="h-9 w-64 rounded-lg" />
<div className="flex items-center gap-2">
<Shimmer className="h-9 w-24 rounded-lg" />
<Shimmer className="h-9 w-28 rounded-lg" />
</div>
</div>
{/* Table Header */}
<div className="grid grid-cols-5 gap-4 px-6 py-3.5 bg-slate-50 border-b border-slate-100">
{Array.from({ length: columns }).map((_, i) => (
<ShimmerText
key={i}
height="h-3.5"
width={i === 0 ? "w-28" : i === columns - 1 ? "w-16 ml-auto" : "w-20"}
/>
))}
</div>
{/* Table Rows */}
<div className="divide-y divide-slate-100">
{Array.from({ length: rows }).map((_, rowIdx) => (
<div
key={rowIdx}
className="grid grid-cols-5 gap-4 px-6 py-4 items-center"
>
{/* Column 1: Primary Entity (Avatar + Text) */}
<div className="flex items-center gap-3">
<ShimmerCircle size="w-8 h-8" />
<div className="space-y-1.5 flex-1">
<ShimmerText height="h-4" width="w-32" />
<ShimmerText height="h-3" width="w-24" />
</div>
</div>
{/* Column 2: Status Badge */}
<div>
<Shimmer className="h-6 w-20 rounded-full" />
</div>
{/* Column 3: Date / Timestamp */}
<div>
<ShimmerText height="h-3.5" width="w-24" />
</div>
{/* Column 4: Value / Metric */}
<div>
<ShimmerText height="h-3.5" width="w-16" />
</div>
{/* Column 5: Action Menu */}
<div className="flex justify-end gap-2">
<Shimmer className="h-8 w-8 rounded-md" />
<Shimmer className="h-8 w-8 rounded-md" />
</div>
</div>
))}
</div>
</div>
);
}
Pattern 3: Catalog & Resource Cards
Used for public listings, item showcases, media cards, or blog entries.
export function CardGridSkeleton({ count = 6 }: { count?: number }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: count }).map((_, idx) => (
<div
key={idx}
className="bg-white rounded-2xl p-6 border border-slate-200/80 shadow-sm space-y-4"
>
{/* Card Media / Banner */}
<Shimmer className="w-full h-44 rounded-xl" />
{/* Badges / Meta */}
<div className="flex items-center gap-2">
<Shimmer className="h-5 w-16 rounded-full" />
<ShimmerText height="h-3.5" width="w-20" />
</div>
{/* Title & Body */}
<div className="space-y-2">
<ShimmerText height="h-5" width="w-4/5" />
<ShimmerText height="h-3.5" width="w-full" />
<ShimmerText height="h-3.5" width="w-3/4" />
</div>
{/* Footer Action */}
<div className="pt-2 border-t border-slate-100 flex items-center justify-between">
<ShimmerText height="h-4" width="w-24" />
<Shimmer className="h-8 w-24 rounded-lg" />
</div>
</div>
))}
</div>
);
}
Pattern 4: Document & Entity Detail View
Used for reading single records, audit reports, detailed profiles, or contract views.
export function DocumentDetailSkeleton() {
return (
<div className="max-w-4xl mx-auto space-y-6">
{/* Back link & Top Bar */}
<div className="flex items-center justify-between">
<ShimmerText height="h-4" width="w-24" />
<div className="flex gap-2">
<Shimmer className="h-9 w-20 rounded-lg" />
<Shimmer className="h-9 w-28 rounded-lg" />
</div>
</div>
{/* Main Document Container */}
<div className="bg-white rounded-2xl p-8 border border-slate-200/80 shadow-sm space-y-6">
{/* Document Header */}
<div className="space-y-3 pb-6 border-b border-slate-100">
<div className="flex items-center gap-2">
<Shimmer className="h-6 w-20 rounded-full" />
<ShimmerText height="h-4" width="w-32" />
</div>
<ShimmerText height="h-8" width="w-3/4" />
<ShimmerText height="h-4" width="w-1/2" />
</div>
{/* Highlights / Metrics Grid */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 p-4 rounded-xl bg-slate-50 border border-slate-100">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="space-y-1.5">
<ShimmerText height="h-3" width="w-16" />
<ShimmerText height="h-6" width="w-24" />
</div>
))}
</div>
{/* Longform Text Blocks */}
<div className="space-y-3 pt-2">
<ShimmerText height="h-4" width="w-full" />
<ShimmerText height="h-4" width="w-11/12" />
<ShimmerText height="h-4" width="w-4/5" />
<ShimmerText height="h-4" width="w-full" />
<ShimmerText height="h-4" width="w-2/3" />
</div>
</div>
</div>
);
}
5. State Machine & Implementation Workflow
Always adhere to this state transition structure in components:
export default function ResourceClient() {
const [data, setData] = useState<Resource[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
async function loadData() {
try {
setLoading(true);
setError(null);
const res = await fetchResources();
if (isMounted) {
setData(res.data || []);
}
} catch (err: any) {
if (isMounted) {
setError(err?.message || "Failed to load records");
setData([]); // Clean empty fallback, never dummy data
}
} finally {
if (isMounted) {
setLoading(false);
}
}
}
loadData();
return () => {
isMounted = false;
};
}, []);
// 1. Loading State -> Content-Matched Shimmer
if (loading) {
return <ResourceGridSkeleton />;
}
// 2. Error State -> Actionable alert with retry
if (error) {
return <ResourceErrorState message={error} onRetry={() => loadData()} />;
}
// 3. Empty State -> Clean, guidance-oriented UI
if (data.length === 0) {
return <ResourceEmptyState />;
}
// 4. Populated State -> Real rendered content
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{data.map((item) => (
<ResourceCard key={item.id} item={item} />
))}
</div>
);
}
6. Implementation & Audit Checklist
Before considering an interface complete:
- No Generic Spinners for Primary Layouts: Replaced full-page loaders or
<Loader2 className="animate-spin" />blocks with structural skeletons. (Small inline spinners on save buttons remain acceptable). - Geometry Alignment: Verify that skeleton card heights, padding, and flex/grid gaps match the populated components within ±4px.
- Responsive Breakpoints: Ensure skeleton containers use the identical responsive classes (
sm:,md:,lg:) as the active views. - Zero Mock Data in Codebase:
- Grepped for
const mock*,const fallback*,const sample*,dummyData. - Verified that network failures resolve to clean empty states or error alerts, not fabricated test data.
- Grepped for
- Motion Sensitivity: Tested with
prefers-reduced-motion: reducein browser dev tools to confirm the gradient animation stops smoothly. - Build Validation: Executed TypeScript and framework build tests (
npm run build) to ensure zero type errors or unused imports.