Profile Header
Original · freeprofile header with cover and avatar
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/profile-header.json"use client";
import { useCallback, useId, useMemo, useRef, useState } from "react";
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
type TabId = "overview" | "projects" | "activity" | "about";
type Tab = {
id: TabId;
label: string;
count?: number;
};
type Stat = {
label: string;
value: string;
hint: string;
};
const TABS: Tab[] = [
{ id: "overview", label: "Overview" },
{ id: "projects", label: "Projects", count: 24 },
{ id: "activity", label: "Activity", count: 8 },
{ id: "about", label: "About" },
];
const STATS: Stat[] = [
{ label: "Followers", value: "12,400", hint: "+318 this month" },
{ label: "Following", value: "486", hint: "Across 12 teams" },
{ label: "Shipped", value: "24", hint: "Since Jan 2023" },
];
const PANELS: Record<TabId, { heading: string; body: string; chips: string[] }> = {
overview: {
heading: "Design engineer, mostly interfaces",
body: "I build the parts of a product people actually touch — component systems, motion, and the boring accessibility work that makes them usable. Currently leading the design systems guild at Northbeam, where we cut our component count from 340 to 96 without shipping a single visual regression.",
chips: ["Design Systems", "TypeScript", "Motion", "Accessibility"],
},
projects: {
heading: "24 public projects",
body: "Recent work includes Prism (a token pipeline that syncs Figma variables to CSS custom properties in under 400ms), Understudy (a headless focus-management library), and the Northbeam pattern library. Most of it is MIT licensed and gets more issues than stars, which I take as a compliment.",
chips: ["Prism", "Understudy", "Northbeam UI", "kbd-trap"],
},
activity: {
heading: "Eight updates this week",
body: "Merged the roving-tabindex rewrite in Understudy, reviewed 11 pull requests, and finally closed the four-month-old issue about Safari's focus ring clipping inside overflow containers. The fix was two lines. Finding it was not.",
chips: ["11 reviews", "3 releases", "1 very old bug"],
},
about: {
heading: "Based in Lisbon, from Karachi",
body: "Ten years in, still convinced the hardest problem in frontend is naming things. I speak at a few conferences a year, write about interface craft, and answer every email eventually. Open to advisory work with teams building tools for other engineers.",
chips: ["Lisbon, PT", "Advisory open", "Speaks at conferences"],
},
};
export default function ProfileHeader() {
const reduced = useReducedMotion();
const uid = useId();
const [activeTab, setActiveTab] = useState<TabId>("overview");
const [following, setFollowing] = useState(false);
const [copied, setCopied] = useState(false);
const tabRefs = useRef<Record<string, HTMLButtonElement | null>>({});
const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const followerCount = useMemo(() => (following ? "12,401" : "12,400"), [following]);
const onTabKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLButtonElement>, index: number) => {
const keys = ["ArrowRight", "ArrowLeft", "Home", "End"];
if (!keys.includes(event.key)) return;
event.preventDefault();
let next = index;
if (event.key === "ArrowRight") next = (index + 1) % TABS.length;
if (event.key === "ArrowLeft") next = (index - 1 + TABS.length) % TABS.length;
if (event.key === "Home") next = 0;
if (event.key === "End") next = TABS.length - 1;
const target = TABS[next];
setActiveTab(target.id);
tabRefs.current[target.id]?.focus();
},
[],
);
const handleCopy = useCallback(() => {
const handle = "@rhea.builds";
if (copyTimer.current) clearTimeout(copyTimer.current);
const done = () => {
setCopied(true);
copyTimer.current = setTimeout(() => setCopied(false), 2000);
};
if (typeof navigator !== "undefined" && navigator.clipboard) {
navigator.clipboard.writeText(handle).then(done).catch(done);
} else {
done();
}
}, []);
const panel = PANELS[activeTab];
return (
<section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-20 dark:bg-slate-950">
<style>{`
@keyframes phdr-drift {
0% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(-3%, 2%, 0) scale(1.06); }
100% { transform: translate3d(0, 0, 0) scale(1); }
}
@keyframes phdr-sheen {
0% { transform: translateX(-120%) skewX(-18deg); opacity: 0; }
40% { opacity: 0.5; }
100% { transform: translateX(240%) skewX(-18deg); opacity: 0; }
}
@keyframes phdr-pulse-ring {
0% { transform: scale(1); opacity: 0.55; }
70% { transform: scale(2.1); opacity: 0; }
100% { transform: scale(2.1); opacity: 0; }
}
.phdr-drift { animation: phdr-drift 22s ease-in-out infinite; }
.phdr-sheen { animation: phdr-sheen 7s ease-in-out 1.5s infinite; }
.phdr-ring { animation: phdr-pulse-ring 2.6s ease-out infinite; }
@media (prefers-reduced-motion: reduce) {
.phdr-drift, .phdr-sheen, .phdr-ring {
animation: none !important;
}
.phdr-sheen { opacity: 0; }
}
`}</style>
<div className="mx-auto w-full max-w-5xl">
<div className="overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
{/* Cover */}
<div className="relative h-40 overflow-hidden sm:h-56">
<div className="phdr-drift absolute -inset-16 bg-[radial-gradient(60%_80%_at_20%_20%,#6366f1_0%,transparent_60%),radial-gradient(55%_75%_at_80%_30%,#8b5cf6_0%,transparent_62%),radial-gradient(70%_90%_at_55%_95%,#10b981_0%,transparent_58%)] opacity-90 dark:opacity-70" />
<div className="absolute inset-0 bg-[linear-gradient(115deg,rgba(15,23,42,0.05)_0%,rgba(15,23,42,0.35)_100%)]" />
<div
aria-hidden="true"
className="absolute inset-0 opacity-[0.18] [background-image:linear-gradient(to_right,#fff_1px,transparent_1px),linear-gradient(to_bottom,#fff_1px,transparent_1px)] [background-size:28px_28px]"
/>
<div
aria-hidden="true"
className="phdr-sheen absolute inset-y-0 -left-1/3 w-1/3 bg-gradient-to-r from-transparent via-white/45 to-transparent"
/>
<div className="absolute right-4 top-4 flex items-center gap-2 sm:right-6 sm:top-6">
<span className="inline-flex items-center gap-1.5 rounded-full bg-white/15 px-3 py-1 text-xs font-medium text-white ring-1 ring-inset ring-white/25 backdrop-blur-sm">
<span className="relative flex h-2 w-2">
<span className="phdr-ring absolute inline-flex h-full w-full rounded-full bg-emerald-300" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-400" />
</span>
Open to advisory work
</span>
</div>
</div>
{/* Identity row */}
<div className="px-5 pb-6 sm:px-8">
<div className="flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between">
<div className="flex items-end gap-4">
<div className="-mt-12 shrink-0 sm:-mt-16">
<div className="relative">
<div className="grid h-24 w-24 place-items-center rounded-2xl bg-gradient-to-br from-indigo-500 via-violet-500 to-emerald-400 text-2xl font-semibold tracking-tight text-white ring-4 ring-white sm:h-28 sm:w-28 sm:text-3xl dark:ring-slate-900">
RA
</div>
<span
className="absolute -bottom-1 -right-1 grid h-7 w-7 place-items-center rounded-full bg-white ring-2 ring-white dark:bg-slate-900 dark:ring-slate-900"
title="Verified maintainer"
>
<svg
viewBox="0 0 20 20"
aria-hidden="true"
className="h-5 w-5 text-sky-500"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M10 1.5l2.1 1.6 2.6-.2 1 2.4 2.3 1.3-.7 2.5.7 2.5-2.3 1.3-1 2.4-2.6-.2L10 18.5l-2.1-1.6-2.6.2-1-2.4L2 13.4l.7-2.5L2 8.4l2.3-1.3 1-2.4 2.6.2L10 1.5zm3.4 6.2a.9.9 0 00-1.3-1.2l-3 3.1-1.2-1.2a.9.9 0 10-1.3 1.3l1.9 1.8a.9.9 0 001.3 0l3.6-3.8z"
clipRule="evenodd"
/>
</svg>
<span className="sr-only">Verified maintainer</span>
</span>
</div>
</div>
<div className="min-w-0 pb-1">
<h1 className="truncate text-xl font-semibold tracking-tight text-slate-900 sm:text-2xl dark:text-slate-50">
Rhea Alvarez
</h1>
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-slate-500 dark:text-slate-400">
<button
type="button"
onClick={handleCopy}
className="group inline-flex items-center gap-1.5 rounded-md px-1 py-0.5 font-medium text-slate-600 transition-colors hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-300 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-900"
aria-label="Copy handle @rhea.builds to clipboard"
>
@rhea.builds
{copied ? (
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-3.5 w-3.5 text-emerald-500" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 10.5l4 4 8-9" />
</svg>
) : (
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-3.5 w-3.5 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100" fill="none" stroke="currentColor" strokeWidth="1.6">
<rect x="7" y="7" width="9" height="9" rx="2" />
<path d="M13 5.5A2.5 2.5 0 0010.5 3h-4A3.5 3.5 0 003 6.5v4A2.5 2.5 0 005.5 13" strokeLinecap="round" />
</svg>
)}
</button>
<span aria-hidden="true" className="text-slate-300 dark:text-slate-600">
·
</span>
<span className="inline-flex items-center gap-1">
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="1.6">
<path d="M10 18s6-4.9 6-9a6 6 0 10-12 0c0 4.1 6 9 6 9z" />
<circle cx="10" cy="9" r="2.2" />
</svg>
Lisbon, Portugal
</span>
</div>
<p aria-live="polite" className="sr-only">
{copied ? "Handle copied to clipboard" : ""}
</p>
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
<motion.button
type="button"
aria-pressed={following}
onClick={() => setFollowing((v) => !v)}
whileTap={reduced ? undefined : { scale: 0.96 }}
className={[
"inline-flex h-10 items-center gap-2 rounded-xl px-4 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
following
? "bg-slate-100 text-slate-700 ring-1 ring-inset ring-slate-200 hover:bg-slate-200 focus-visible:ring-slate-400 dark:bg-slate-800 dark:text-slate-200 dark:ring-slate-700 dark:hover:bg-slate-700"
: "bg-indigo-600 text-white hover:bg-indigo-500 focus-visible:ring-indigo-500",
].join(" ")}
>
{following ? (
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 10.5l4 4 8-9" />
</svg>
) : (
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M10 4.5v11M4.5 10h11" />
</svg>
)}
{following ? "Following" : "Follow"}
</motion.button>
<a
href="mailto:rhea@rhea.builds"
className="inline-flex h-10 items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
<svg viewBox="0 0 20 20" aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="1.6">
<rect x="2.5" y="4.5" width="15" height="11" rx="2" />
<path d="M3 6l7 5 7-5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Message
</a>
</div>
</div>
{/* Stats */}
<dl className="mt-6 grid grid-cols-3 gap-px overflow-hidden rounded-xl bg-slate-200 dark:bg-slate-800">
{STATS.map((stat) => (
<div key={stat.label} className="bg-white px-4 py-3 dark:bg-slate-900">
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
{stat.label}
</dt>
<dd className="mt-0.5 text-lg font-semibold tabular-nums text-slate-900 dark:text-slate-50">
{stat.label === "Followers" ? followerCount : stat.value}
</dd>
<dd className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">{stat.hint}</dd>
</div>
))}
</dl>
</div>
{/* Tabs */}
<div className="border-t border-slate-200 dark:border-slate-800">
<div
role="tablist"
aria-label="Profile sections"
className="flex gap-1 overflow-x-auto px-3 sm:px-6"
>
{TABS.map((tab, index) => {
const selected = tab.id === activeTab;
return (
<button
key={tab.id}
ref={(el) => {
tabRefs.current[tab.id] = el;
}}
role="tab"
type="button"
id={`${uid}-tab-${tab.id}`}
aria-selected={selected}
aria-controls={`${uid}-panel-${tab.id}`}
tabIndex={selected ? 0 : -1}
onClick={() => setActiveTab(tab.id)}
onKeyDown={(e) => onTabKeyDown(e, index)}
className={[
"relative shrink-0 rounded-lg px-3 py-3 text-sm font-medium transition-colors 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",
selected
? "text-slate-900 dark:text-slate-50"
: "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200",
].join(" ")}
>
<span className="inline-flex items-center gap-1.5">
{tab.label}
{typeof tab.count === "number" && (
<span className="rounded-md bg-slate-100 px-1.5 py-0.5 text-xs font-semibold tabular-nums text-slate-600 dark:bg-slate-800 dark:text-slate-300">
{tab.count}
</span>
)}
</span>
{selected && (
<motion.span
layoutId={reduced ? undefined : `${uid}-tab-underline`}
transition={reduced ? { duration: 0 } : { type: "spring", stiffness: 420, damping: 34 }}
className="absolute inset-x-2 -bottom-px h-0.5 rounded-full bg-indigo-600 dark:bg-indigo-400"
/>
)}
</button>
);
})}
</div>
</div>
{/* Panel */}
<div className="border-t border-slate-200 bg-slate-50/60 px-5 py-6 sm:px-8 sm:py-7 dark:border-slate-800 dark:bg-slate-950/40">
<motion.div
key={activeTab}
id={`${uid}-panel-${activeTab}`}
role="tabpanel"
aria-labelledby={`${uid}-tab-${activeTab}`}
tabIndex={0}
initial={reduced ? false : { opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={reduced ? { duration: 0 } : { duration: 0.22, ease: "easeOut" }}
className="rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
>
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-50">
{panel.heading}
</h2>
<p className="mt-2 max-w-2xl text-sm leading-relaxed text-slate-600 dark:text-slate-300">
{panel.body}
</p>
<ul className="mt-4 flex flex-wrap gap-2">
{panel.chips.map((chip) => (
<li
key={chip}
className="rounded-full border border-slate-200 bg-white px-2.5 py-1 text-xs font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300"
>
{chip}
</li>
))}
</ul>
</motion.div>
</div>
</div>
</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 →
Profile Settings
Originalaccount settings panel

Profile User Card
Originalcompact user profile card

Profile Stats
Originalprofile with stats and badges

Profile Edit
Originaledit profile form

Profile Team
Originalteam member profile grid

Profile Social
Originalsocial profile with follow

Profile Tabs
Originalprofile page with tabbed content

Profile Bio
Originalbio card with links

Profile Cover
Originallarge cover profile hero

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.

