Skip to main content
Back to Blog
Tutorials
2 min read
November 17, 2024

How to Create Loading Skeletons in React

Build loading skeleton components that improve perceived performance. Shimmer effects, accessible states, and Next.js loading.tsx integration.

Ryel Banfield

Founder & Lead Developer

Loading skeletons replace spinners with placeholder shapes that match the layout of real content. They reduce perceived load time and prevent layout shift.

Step 1: Base Skeleton Component

// components/ui/skeleton.tsx
import { cn } from "@/lib/utils";

function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn("animate-pulse rounded-md bg-gray-200 dark:bg-gray-800", className)}
      {...props}
    />
  );
}

export { Skeleton };

Step 2: Skeleton Variants

// Text skeleton
<Skeleton className="h-4 w-3/4" />

// Heading skeleton
<Skeleton className="h-8 w-1/2" />

// Circle (avatar)
<Skeleton className="h-12 w-12 rounded-full" />

// Image placeholder
<Skeleton className="h-48 w-full rounded-lg" />

// Button placeholder
<Skeleton className="h-10 w-32 rounded-lg" />

Step 3: Compose Skeleton Layouts

Card Skeleton

function CardSkeleton() {
  return (
    <div className="rounded-lg border p-6 dark:border-gray-700">
      <Skeleton className="h-48 w-full rounded-lg" />
      <div className="mt-4 space-y-3">
        <Skeleton className="h-4 w-1/4" />
        <Skeleton className="h-6 w-3/4" />
        <Skeleton className="h-4 w-full" />
        <Skeleton className="h-4 w-5/6" />
      </div>
      <div className="mt-4 flex items-center gap-3">
        <Skeleton className="h-8 w-8 rounded-full" />
        <Skeleton className="h-4 w-24" />
      </div>
    </div>
  );
}

Table Row Skeleton

function TableRowSkeleton() {
  return (
    <tr className="border-b dark:border-gray-700">
      <td className="px-4 py-3"><Skeleton className="h-4 w-20" /></td>
      <td className="px-4 py-3"><Skeleton className="h-4 w-32" /></td>
      <td className="px-4 py-3"><Skeleton className="h-4 w-16" /></td>
      <td className="px-4 py-3"><Skeleton className="h-6 w-16 rounded-full" /></td>
      <td className="px-4 py-3"><Skeleton className="h-4 w-24" /></td>
    </tr>
  );
}

function TableSkeleton({ rows = 5 }: { rows?: number }) {
  return (
    <table className="w-full">
      <thead>
        <tr className="border-b dark:border-gray-700">
          {["w-20", "w-32", "w-16", "w-16", "w-24"].map((w, i) => (
            <th key={i} className="px-4 py-3">
              <Skeleton className={`h-4 ${w}`} />
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {Array.from({ length: rows }).map((_, i) => (
          <TableRowSkeleton key={i} />
        ))}
      </tbody>
    </table>
  );
}

Profile Skeleton

function ProfileSkeleton() {
  return (
    <div className="flex items-center gap-4">
      <Skeleton className="h-16 w-16 rounded-full" />
      <div className="space-y-2">
        <Skeleton className="h-5 w-40" />
        <Skeleton className="h-4 w-32" />
        <Skeleton className="h-4 w-48" />
      </div>
    </div>
  );
}

Step 4: Add Shimmer Effect

For a more polished loading state, add a shimmer animation:

/* globals.css */
@keyframes shimmer {
  0% {
    background-position: -200% 0;
  }
  100% {
    background-position: 200% 0;
  }
}

.skeleton-shimmer {
  background: linear-gradient(
    90deg,
    rgb(229 231 235) 25%,
    rgb(243 244 246) 37%,
    rgb(229 231 235) 63%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s ease-in-out infinite;
}

.dark .skeleton-shimmer {
  background: linear-gradient(
    90deg,
    rgb(31 41 55) 25%,
    rgb(55 65 81) 37%,
    rgb(31 41 55) 63%
  );
  background-size: 200% 100%;
}
function ShimmerSkeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn("skeleton-shimmer rounded-md", className)}
      {...props}
    />
  );
}

Step 5: Use with Next.js loading.tsx

Next.js automatically shows loading.tsx while a route segment loads:

// app/blog/loading.tsx
import { Skeleton } from "@/components/ui/skeleton";

export default function BlogLoading() {
  return (
    <main className="mx-auto max-w-7xl px-6 py-12">
      <Skeleton className="h-10 w-48" />
      <Skeleton className="mt-2 h-5 w-96" />
      <div className="mt-8 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
        {Array.from({ length: 6 }).map((_, i) => (
          <CardSkeleton key={i} />
        ))}
      </div>
    </main>
  );
}

Step 6: Suspense Boundaries

Use React Suspense for component-level loading:

import { Suspense } from "react";

export default function DashboardPage() {
  return (
    <div className="grid gap-6 lg:grid-cols-3">
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>
      <Suspense fallback={<TableSkeleton rows={10} />}>
        <RecentOrders />
      </Suspense>
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>
    </div>
  );
}

Accessibility

Add proper ARIA attributes:

function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      role="status"
      aria-label="Loading"
      className={cn("animate-pulse rounded-md bg-gray-200 dark:bg-gray-800", className)}
      {...props}
    >
      <span className="sr-only">Loading...</span>
    </div>
  );
}

Best Practices

  • Match skeleton shapes to actual content layout
  • Use consistent timing (1-2 seconds for the pulse animation)
  • Do not show skeletons for instant loads (< 200ms)
  • Avoid skeleton for error states
  • Test skeleton layouts against actual content to prevent layout shift

Need Help With UX?

We design and build applications with polished loading states and optimized user experiences. Contact us to discuss your project.

Reactloading statesskeletonsUXtutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles