Skeleton Loader
Shimmering placeholders that swap to real content when loaded.
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>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/skeleton-loader.tsxcomponents/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> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.