Web InnoventixFreeCode

Profile Header

Original · free

profile header with cover and avatar

byWeb InnoventixReact + Tailwind
profileheaderprofiles
Open

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
profile-header.tsx
"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

motion

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 quote

Similar components

Browse all →