Home/Fast Coding Skills/content-matched-shimmer-skeleton-patterns
Frontend9 min read0 downloads

Content-Matched Shimmer Skeleton Patterns

Architectural patterns for building high-fidelity, content-matched shimmer skeletons in React apps, eliminating mock data and preventing layout shifts.

#React#CSS#Skeleton#Loading#Accessibility
Descriptive File Name·0 downloads
content-matched-shimmer-skeleton-patterns.md

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 [] or null, 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:

  1. 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).
  2. Geometry Alignment: Verify that skeleton card heights, padding, and flex/grid gaps match the populated components within ±4px.
  3. Responsive Breakpoints: Ensure skeleton containers use the identical responsive classes (sm:, md:, lg:) as the active views.
  4. 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.
  5. Motion Sensitivity: Tested with prefers-reduced-motion: reduce in browser dev tools to confirm the gradient animation stops smoothly.
  6. Build Validation: Executed TypeScript and framework build tests (npm run build) to ensure zero type errors or unused imports.
Save this skill as content-matched-shimmer-skeleton-patterns.md in your repository or AI editor config.