Skeleton Loader

Shimmering placeholders that swap to real content when loaded.

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/skeleton-loader.tsx

    components/ui/skeleton-loader.tsx
    "use client";
    
    import { AnimatePresence, motion } from "motion/react";
    import { useEffect, useState } from "react";
    
    export function Skeleton({ className = "" }: { className?: string }) {
      return (
        <div aria-hidden className={`relative overflow-hidden rounded-lg bg-zinc-100 ${className}`}>
          <motion.div
            className="absolute inset-0 bg-gradient-to-r from-transparent via-white/70 to-transparent"
            animate={{ x: ["-100%", "100%"] }}
            transition={{ duration: 1.4, repeat: Infinity, ease: "easeInOut" }}
          />
        </div>
      );
    }
    
    function CardSkeleton() {
      return (
        <div className="flex gap-4 rounded-2xl border border-zinc-200 bg-white p-5" role="status" aria-label="Loading">
          <Skeleton className="size-12 shrink-0 rounded-full" />
          <div className="flex-1 space-y-2.5">
            <Skeleton className="h-4 w-1/3" />
            <Skeleton className="h-3 w-full" />
            <Skeleton className="h-3 w-4/5" />
          </div>
        </div>
      );
    }
    
    function Card() {
      return (
        <div className="flex gap-4 rounded-2xl border border-zinc-200 bg-white p-5">
          <span className="grid size-12 shrink-0 place-items-center rounded-full bg-gradient-to-br from-blue-500 to-violet-600 font-semibold text-white">PR</span>
          <div>
            <p className="font-medium text-zinc-900">Priya Raman</p>
            <p className="mt-1 text-sm text-zinc-500">Shipped the new onboarding flow — activation is up 14% in the first week.</p>
          </div>
        </div>
      );
    }
    
    export default function Demo() {
      const [loading, setLoading] = useState(true);
      useEffect(() => {
        if (!loading) return;
        const t = setTimeout(() => setLoading(false), 2200);
        return () => clearTimeout(t);
      }, [loading]);
    
      return (
        <div className="w-full max-w-md space-y-3">
          <AnimatePresence mode="wait">
            <motion.div key={loading ? "s" : "c"} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
              {loading ? <CardSkeleton /> : <Card />}
            </motion.div>
          </AnimatePresence>
          <button onClick={() => setLoading(true)} className="text-sm text-blue-600 hover:underline">
            Reload
          </button>
        </div>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.