Profile Skeleton
Original · freeprofile header skeleton
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/skel-profile.json"use client";
import { useEffect, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
type Density = "detailed" | "compact";
const DENSITY_ORDER: Density[] = ["detailed", "compact"];
const STATS: { label: string; value: string }[] = [
{ label: "Followers", value: "12.4k" },
{ label: "Following", value: "384" },
{ label: "Posts", value: "271" },
];
function Skeleton({
shimmer,
className,
rounded = "rounded-md",
}: {
shimmer: boolean;
className?: string;
rounded?: string;
}) {
return (
<span
aria-hidden="true"
className={[
"block bg-gradient-to-r from-slate-200 via-slate-100 to-slate-200",
"dark:from-slate-800 dark:via-slate-700/70 dark:to-slate-800",
"[background-size:200%_100%]",
shimmer ? "skelprof-shimmer" : "",
rounded,
className ?? "",
].join(" ")}
/>
);
}
function VerifiedIcon() {
return (
<svg
viewBox="0 0 24 24"
aria-hidden="true"
className="h-5 w-5 text-sky-500"
fill="currentColor"
>
<path d="M12 1.5l2.35 1.7 2.9-.02 1.02 2.72 2.53 1.42-.55 2.85 1.42 2.53-2.02 2.08.02 2.9-2.85.62-1.7 2.35-2.72-1.01-2.79 1.03-1.72-2.34-2.9-.6-.62-2.85L1.5 15.4l1.03-2.79-1.03-2.72 2.34-1.72.6-2.9 2.85-.62L9.4 1.5l2.6 1.03z" />
<path
d="M8 12.2l2.6 2.6L16 9.4"
fill="none"
stroke="#fff"
strokeWidth="2.1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function PinIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 21s7-6.3 7-11a7 7 0 10-14 0c0 4.7 7 11 7 11z" />
<circle cx="12" cy="10" r="2.5" />
</svg>
);
}
function CalendarIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="5" width="18" height="16" rx="2" />
<path d="M3 9h18M8 3v4M16 3v4" />
</svg>
);
}
function LinkIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M10 13a5 5 0 007.07 0l2.12-2.12a5 5 0 00-7.07-7.07L10.59 5.3" />
<path d="M14 11a5 5 0 00-7.07 0L4.81 13.1a5 5 0 007.07 7.07l1.53-1.5" />
</svg>
);
}
function PlusIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 5v14M5 12h14" />
</svg>
);
}
function CheckIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 6L9 17l-5-5" />
</svg>
);
}
function MessageIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a3 3 0 01-3 3H8l-5 4V6a3 3 0 013-3h12a3 3 0 013 3z" />
</svg>
);
}
function RefreshIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 12a9 9 0 0115.5-6.3L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 01-15.5 6.3L3 16" />
<path d="M3 21v-5h5" />
</svg>
);
}
export default function SkelProfile() {
const reduce = useReducedMotion();
const [loading, setLoading] = useState(true);
const [shimmerPref, setShimmerPref] = useState(true);
const [density, setDensity] = useState<Density>("detailed");
const [following, setFollowing] = useState(false);
const optionRefs = useRef<(HTMLButtonElement | null)[]>([]);
const shimmer = shimmerPref && !reduce;
useEffect(() => {
if (!loading) return;
const id = window.setTimeout(() => setLoading(false), 2200);
return () => window.clearTimeout(id);
}, [loading]);
function reload() {
setFollowing(false);
setLoading(true);
}
function onDensityKey(e: React.KeyboardEvent<HTMLDivElement>) {
const idx = DENSITY_ORDER.indexOf(density);
let next = idx;
switch (e.key) {
case "ArrowRight":
case "ArrowDown":
next = (idx + 1) % DENSITY_ORDER.length;
break;
case "ArrowLeft":
case "ArrowUp":
next = (idx - 1 + DENSITY_ORDER.length) % DENSITY_ORDER.length;
break;
case "Home":
next = 0;
break;
case "End":
next = DENSITY_ORDER.length - 1;
break;
default:
return;
}
e.preventDefault();
setDensity(DENSITY_ORDER[next]);
optionRefs.current[next]?.focus();
}
const isDetailed = density === "detailed";
const avatarSize = isDetailed ? "h-24 w-24" : "h-16 w-16";
const ring =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";
return (
<section className="relative w-full bg-slate-50 px-5 py-16 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:px-8 sm:py-24">
<style>{`
@keyframes skelprof-sweep {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.skelprof-shimmer { animation: skelprof-sweep 1.6s linear infinite; }
@keyframes skelprof-spin { to { transform: rotate(360deg); } }
.skelprof-spin { animation: skelprof-spin 0.9s linear infinite; }
@media (prefers-reduced-motion: reduce) {
.skelprof-shimmer, .skelprof-spin { animation: none !important; }
}
`}</style>
<div className="mx-auto w-full max-w-2xl">
<header className="mb-8">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
Loading states
</p>
<h2 className="mt-2 text-2xl font-bold tracking-tight sm:text-3xl">
Profile header skeleton
</h2>
<p className="mt-2 max-w-prose text-sm leading-relaxed text-slate-600 dark:text-slate-400">
A placeholder that mirrors the real layout so nothing jumps when data
arrives. Reload to replay the skeleton, switch density, or follow to
feel the interaction.
</p>
</header>
{/* Controls */}
<div className="mb-6 flex flex-wrap items-center gap-3">
<button
type="button"
onClick={reload}
disabled={loading}
className={`inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-3.5 py-2 text-sm font-semibold text-slate-800 shadow-sm transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-60 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800 ${ring}`}
>
<span className={loading ? "skelprof-spin" : ""}>
<RefreshIcon />
</span>
{loading ? "Loading…" : "Reload"}
</button>
<div
role="radiogroup"
aria-label="Layout density"
onKeyDown={onDensityKey}
className="inline-flex rounded-lg bg-slate-100 p-1 dark:bg-slate-800"
>
{DENSITY_ORDER.map((option, i) => {
const selected = density === option;
return (
<button
key={option}
ref={(el) => {
optionRefs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={selected}
tabIndex={selected ? 0 : -1}
onClick={() => setDensity(option)}
className={`rounded-md px-3 py-1.5 text-sm font-semibold capitalize transition ${ring} ${
selected
? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-white"
: "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
}`}
>
{option}
</button>
);
})}
</div>
<button
type="button"
role="switch"
aria-checked={shimmerPref}
aria-label="Shimmer animation"
onClick={() => setShimmerPref((v) => !v)}
disabled={reduce}
className={`ml-auto inline-flex items-center gap-2.5 text-sm font-medium text-slate-600 disabled:opacity-60 dark:text-slate-300 ${ring} rounded-full`}
>
<span
className={`relative inline-block h-6 w-10 rounded-full transition-colors ${
shimmerPref
? "bg-indigo-600"
: "bg-slate-300 dark:bg-slate-700"
}`}
>
<span
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform ${
shimmerPref ? "translate-x-4" : "translate-x-0.5"
}`}
/>
</span>
Shimmer
</button>
</div>
{/* Card */}
<div
aria-busy={loading}
className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
>
<p className="sr-only" role="status" aria-live="polite">
{loading ? "Loading profile" : "Profile loaded"}
</p>
{/* Banner (detailed only) */}
{isDetailed &&
(loading ? (
<Skeleton shimmer={shimmer} rounded="rounded-none" className="h-28 w-full sm:h-32" />
) : (
<div className="h-28 w-full bg-gradient-to-r from-indigo-500 via-violet-500 to-sky-500 sm:h-32" />
))}
<div className={isDetailed ? "px-6 pb-6 sm:px-8" : "p-6 sm:p-8"}>
{isDetailed ? (
<>
{/* Avatar + actions row */}
<div className="-mt-12 flex items-end justify-between gap-4">
{loading ? (
<Skeleton
shimmer={shimmer}
rounded="rounded-full"
className={`${avatarSize} ring-4 ring-white dark:ring-slate-900`}
/>
) : (
<div
className={`flex ${avatarSize} items-center justify-center rounded-full bg-gradient-to-br from-indigo-500 to-violet-600 text-2xl font-bold text-white ring-4 ring-white dark:ring-slate-900`}
aria-hidden="true"
>
AO
</div>
)}
<div className="mb-1 flex items-center gap-2">
{loading ? (
<>
<Skeleton shimmer={shimmer} rounded="rounded-lg" className="h-9 w-24" />
<Skeleton shimmer={shimmer} rounded="rounded-lg" className="h-9 w-9" />
</>
) : (
<>
<button
type="button"
aria-pressed={following}
onClick={() => setFollowing((v) => !v)}
className={`inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold shadow-sm transition ${ring} ${
following
? "border border-slate-300 bg-white text-slate-800 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
: "bg-indigo-600 text-white hover:bg-indigo-500"
}`}
>
{following ? <CheckIcon /> : <PlusIcon />}
{following ? "Following" : "Follow"}
</button>
<button
type="button"
aria-label="Send a message"
className={`inline-flex items-center justify-center rounded-lg border border-slate-300 bg-white p-2.5 text-slate-700 shadow-sm transition hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 ${ring}`}
>
<MessageIcon />
</button>
</>
)}
</div>
</div>
{/* Name / handle */}
<div className="mt-4">
{loading ? (
<div className="space-y-2">
<Skeleton shimmer={shimmer} className="h-6 w-48" />
<Skeleton shimmer={shimmer} className="h-4 w-32" />
</div>
) : (
<motion.div
initial={reduce ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<div className="flex items-center gap-1.5">
<h3 className="text-xl font-bold tracking-tight">
Dr. Amara Okonkwo
</h3>
<VerifiedIcon />
</div>
<p className="text-sm text-slate-500 dark:text-slate-400">
@amara.builds
</p>
</motion.div>
)}
</div>
{/* Bio */}
<div className="mt-4">
{loading ? (
<div className="space-y-2">
<Skeleton shimmer={shimmer} className="h-3.5 w-full" />
<Skeleton shimmer={shimmer} className="h-3.5 w-11/12" />
<Skeleton shimmer={shimmer} className="h-3.5 w-2/3" />
</div>
) : (
<p className="max-w-prose text-sm leading-relaxed text-slate-700 dark:text-slate-300">
Staff design engineer building calmer software. I write about
interface systems, motion, and the craft of shipping things
that feel effortless.
</p>
)}
</div>
{/* Meta */}
<div className="mt-4 flex flex-wrap items-center gap-x-5 gap-y-2 text-sm text-slate-500 dark:text-slate-400">
{loading ? (
<>
<Skeleton shimmer={shimmer} className="h-3.5 w-32" />
<Skeleton shimmer={shimmer} className="h-3.5 w-28" />
<Skeleton shimmer={shimmer} className="h-3.5 w-24" />
</>
) : (
<>
<span className="inline-flex items-center gap-1.5">
<PinIcon />
Lisbon, Portugal
</span>
<span className="inline-flex items-center gap-1.5">
<CalendarIcon />
Joined March 2019
</span>
<a
href="https://example.com"
target="_blank"
rel="noopener"
className={`inline-flex items-center gap-1.5 rounded font-medium text-indigo-600 hover:underline dark:text-indigo-400 ${ring}`}
>
<LinkIcon />
amara.build
</a>
</>
)}
</div>
{/* Stats */}
<div className="mt-6 grid grid-cols-3 gap-3 border-t border-slate-200 pt-5 dark:border-slate-800">
{STATS.map((stat) => (
<div key={stat.label} className="text-center">
{loading ? (
<div className="flex flex-col items-center gap-2">
<Skeleton shimmer={shimmer} className="h-5 w-12" />
<Skeleton shimmer={shimmer} className="h-3 w-16" />
</div>
) : (
<>
<div className="text-lg font-bold">{stat.value}</div>
<div className="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400">
{stat.label}
</div>
</>
)}
</div>
))}
</div>
</>
) : (
/* Compact */
<>
<div className="flex items-center gap-4">
{loading ? (
<Skeleton shimmer={shimmer} rounded="rounded-full" className={avatarSize} />
) : (
<div
className={`flex ${avatarSize} shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-indigo-500 to-violet-600 text-lg font-bold text-white`}
aria-hidden="true"
>
AO
</div>
)}
<div className="min-w-0 flex-1">
{loading ? (
<div className="space-y-2">
<Skeleton shimmer={shimmer} className="h-5 w-40" />
<Skeleton shimmer={shimmer} className="h-3.5 w-24" />
</div>
) : (
<motion.div
initial={reduce ? false : { opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, ease: "easeOut" }}
>
<div className="flex items-center gap-1.5">
<h3 className="truncate text-base font-bold">
Dr. Amara Okonkwo
</h3>
<VerifiedIcon />
</div>
<p className="truncate text-sm text-slate-500 dark:text-slate-400">
@amara.builds
</p>
</motion.div>
)}
</div>
{loading ? (
<Skeleton shimmer={shimmer} rounded="rounded-lg" className="h-9 w-24" />
) : (
<button
type="button"
aria-pressed={following}
onClick={() => setFollowing((v) => !v)}
className={`inline-flex shrink-0 items-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold shadow-sm transition ${ring} ${
following
? "border border-slate-300 bg-white text-slate-800 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
: "bg-indigo-600 text-white hover:bg-indigo-500"
}`}
>
{following ? <CheckIcon /> : <PlusIcon />}
{following ? "Following" : "Follow"}
</button>
)}
</div>
<div className="mt-5 flex items-center gap-6 border-t border-slate-200 pt-4 dark:border-slate-800">
{STATS.map((stat) => (
<div key={stat.label} className="flex items-baseline gap-1.5">
{loading ? (
<Skeleton shimmer={shimmer} className="h-4 w-24" />
) : (
<>
<span className="text-sm font-bold">{stat.value}</span>
<span className="text-xs text-slate-500 dark:text-slate-400">
{stat.label}
</span>
</>
)}
</div>
))}
</div>
</>
)}
</div>
</div>
{reduce && (
<p className="mt-3 text-xs text-slate-500 dark:text-slate-400">
Shimmer is paused because your system prefers reduced motion.
</p>
)}
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Card Skeleton
Originalcard skeleton placeholder with shimmer

List Skeleton
Originallist rows skeleton

Article Skeleton
Originalarticle/text skeleton

Table Skeleton
Originaltable skeleton

Gallery Skeleton
Originalimage grid skeleton

Dashboard Skeleton
Originaldashboard widgets skeleton

Comment Skeleton
Originalcomment thread skeleton

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero
OriginalA centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

