Web InnoventixFreeCode

Profile Cover

Original · free

large cover profile hero

byWeb InnoventixReact + Tailwind
profilecoverprofiles
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-cover.json
profile-cover.tsx
"use client";

import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type CoverId = "route190" | "contrail" | "ardennes" | "northshore" | "marine";

type Cover = {
  id: CoverId;
  src: string;
  label: string;
  alt: string;
  place: string;
};

type Stat = {
  label: string;
  value: string;
  hint: string;
};

const COVERS: Cover[] = [
  {
    id: "route190",
    src: "/img/gallery/g20.webp",
    label: "Sunset, Route 190",
    place: "Panamint Valley, CA",
    alt: "A white camper van kicking up dust on a desert dirt road as the sun sets behind low mountains",
  },
  {
    id: "contrail",
    src: "/img/gallery/g12.webp",
    label: "Contrail, 7:40pm",
    place: "Rotterdam, NL",
    alt: "A single aircraft contrail cutting across a deep teal sky above a sunlit bank of cloud",
  },
  {
    id: "ardennes",
    src: "/img/gallery/g05.webp",
    label: "Burn-off",
    place: "Ardennes, BE",
    alt: "Sun burning through heavy fog above the silhouettes of two trees on a ridge",
  },
  {
    id: "northshore",
    src: "/img/gallery/g28.webp",
    label: "North shore, 6am",
    place: "Byron Bay, AU",
    alt: "An empty beach seen through bare branches, with a single figure sitting near breaking waves",
  },
  {
    id: "marine",
    src: "/img/gallery/g33.webp",
    label: "Marine layer",
    place: "Point Dume, CA",
    alt: "A grey marine layer swallowing a curving coastline dotted with distant swimmers",
  },
];

const STATS: Stat[] = [
  { label: "Followers", value: "18,240", hint: "+412 in the last 30 days" },
  { label: "Projects", value: "47", hint: "31 of them still maintained" },
  { label: "Talks", value: "12", hint: "Next: Rotterdam, October" },
];

const CHIPS = ["TypeScript", "Rendering perf", "Virtualised tables", "Focus management"];

const FOLLOWERS_BASE = 18240;
const PROFILE_URL = "https://priya.build/@priyaships";

function CheckBadgeIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-full w-full">
      <path
        d="M12 2.6l2.2 1.7 2.7-.4 1.1 2.5 2.5 1.1-.4 2.7 1.7 2.2-1.7 2.2.4 2.7-2.5 1.1-1.1 2.5-2.7-.4L12 21.4l-2.2-1.7-2.7.4-1.1-2.5-2.5-1.1.4-2.7L2.2 12l1.7-2.2-.4-2.7 2.5-1.1 1.1-2.5 2.7.4L12 2.6z"
        fill="currentColor"
      />
      <path
        d="M8.4 12.2l2.4 2.4 4.8-5"
        stroke="#fff"
        strokeWidth="1.9"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function PinIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <path
        d="M12 21s6.5-5.2 6.5-10a6.5 6.5 0 10-13 0c0 4.8 6.5 10 6.5 10z"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinejoin="round"
      />
      <circle cx="12" cy="11" r="2.4" stroke="currentColor" strokeWidth="1.6" />
    </svg>
  );
}

function LinkIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <path
        d="M10.5 13.5a3.5 3.5 0 005 0l3-3a3.54 3.54 0 00-5-5l-1.2 1.2"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinecap="round"
      />
      <path
        d="M13.5 10.5a3.5 3.5 0 00-5 0l-3 3a3.54 3.54 0 005 5l1.2-1.2"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinecap="round"
      />
    </svg>
  );
}

function CalendarIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <rect x="3.5" y="5.5" width="17" height="15" rx="2.5" stroke="currentColor" strokeWidth="1.6" />
      <path d="M3.5 10.5h17M8.5 3.5v4M15.5 3.5v4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
    </svg>
  );
}

function CameraIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <path
        d="M4 8.5h2.6l1.3-2.2c.2-.4.6-.6 1-.6h6.2c.4 0 .8.2 1 .6l1.3 2.2H20a1.5 1.5 0 011.5 1.5v7.5A1.5 1.5 0 0120 19.5H4a1.5 1.5 0 01-1.5-1.5V10A1.5 1.5 0 014 8.5z"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinejoin="round"
      />
      <circle cx="12" cy="13.5" r="3.2" stroke="currentColor" strokeWidth="1.6" />
    </svg>
  );
}

function CheckIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <path d="M5 12.5l4.5 4.5L19 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function PlusIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
    </svg>
  );
}

function CloseIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
    </svg>
  );
}

function CopyIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
      <rect x="9" y="9" width="11" height="11" rx="2.2" stroke="currentColor" strokeWidth="1.6" />
      <path d="M15 6.5A2.5 2.5 0 0012.5 4H6.5A2.5 2.5 0 004 6.5v6A2.5 2.5 0 006.5 15" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
    </svg>
  );
}

export default function ProfileCover() {
  const reduced = useReducedMotion();
  const uid = useId();

  const [committedCover, setCommittedCover] = useState<CoverId>("route190");
  const [committedFocal, setCommittedFocal] = useState(46);
  const [draftCover, setDraftCover] = useState<CoverId>("route190");
  const [draftFocal, setDraftFocal] = useState(46);
  const [editing, setEditing] = useState(false);
  const [following, setFollowing] = useState(false);
  const [copied, setCopied] = useState(false);
  const [status, setStatus] = useState("");

  const editButtonRef = useRef<HTMLButtonElement | null>(null);
  const thumbRefs = useRef<Array<HTMLButtonElement | null>>([]);
  const copyTimer = useRef<number | null>(null);

  useEffect(() => {
    return () => {
      if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
    };
  }, []);

  const activeId = editing ? draftCover : committedCover;
  const activeFocal = editing ? draftFocal : committedFocal;
  const active = useMemo(() => COVERS.find((c) => c.id === activeId) ?? COVERS[0], [activeId]);

  const followerLabel = useMemo(
    () => (FOLLOWERS_BASE + (following ? 1 : 0)).toLocaleString("en-US"),
    [following],
  );

  const openEditor = useCallback(() => {
    setDraftCover(committedCover);
    setDraftFocal(committedFocal);
    setEditing(true);
  }, [committedCover, committedFocal]);

  const cancelEditor = useCallback(() => {
    setEditing(false);
    setStatus("Cover edit discarded.");
    editButtonRef.current?.focus();
  }, []);

  const saveEditor = useCallback(() => {
    setCommittedCover(draftCover);
    setCommittedFocal(draftFocal);
    setEditing(false);
    const picked = COVERS.find((c) => c.id === draftCover);
    setStatus(`Cover updated to ${picked ? picked.label : "a new photo"}.`);
    editButtonRef.current?.focus();
  }, [draftCover, draftFocal]);

  const onThumbKeyDown = useCallback(
    (event: ReactKeyboardEvent<HTMLDivElement>) => {
      const current = COVERS.findIndex((c) => c.id === draftCover);
      let next = -1;
      if (event.key === "ArrowRight" || event.key === "ArrowDown") next = (current + 1) % COVERS.length;
      else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = (current - 1 + COVERS.length) % COVERS.length;
      else if (event.key === "Home") next = 0;
      else if (event.key === "End") next = COVERS.length - 1;
      else return;
      event.preventDefault();
      setDraftCover(COVERS[next].id);
      thumbRefs.current[next]?.focus();
    },
    [draftCover],
  );

  const onPanelKeyDown = useCallback(
    (event: ReactKeyboardEvent<HTMLDivElement>) => {
      if (event.key === "Escape") {
        event.preventDefault();
        cancelEditor();
      }
    },
    [cancelEditor],
  );

  const handleCopy = useCallback(() => {
    void navigator.clipboard
      .writeText(PROFILE_URL)
      .then(() => {
        setCopied(true);
        setStatus("Profile link copied to clipboard.");
        if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
        copyTimer.current = window.setTimeout(() => setCopied(false), 1800);
      })
      .catch(() => {
        setStatus("Could not copy the profile link.");
      });
  }, []);

  const panelId = `${uid}-cover-panel`;
  const focalId = `${uid}-focal`;
  const ring =
    "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-zinc-950";

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-white to-slate-50 py-16 sm:py-24 dark:from-zinc-950 dark:via-black dark:to-zinc-950">
      <style>{`
        @keyframes pcvr-drift {
          0%   { transform: scale(1.06) translate3d(0, 0, 0); }
          50%  { transform: scale(1.11) translate3d(-1.4%, -1%, 0); }
          100% { transform: scale(1.06) translate3d(0, 0, 0); }
        }
        @keyframes pcvr-sheen {
          0%   { transform: translateX(-140%) skewX(-18deg); opacity: 0; }
          18%  { opacity: 0.9; }
          100% { transform: translateX(240%) skewX(-18deg); opacity: 0; }
        }
        @keyframes pcvr-pulse {
          0%, 100% { opacity: 0.55; }
          50%      { opacity: 1; }
        }
        .pcvr-drift { animation: pcvr-drift 26s ease-in-out infinite; will-change: transform; }
        .pcvr-sheen { animation: pcvr-sheen 4.6s ease-in-out infinite; }
        .pcvr-pulse { animation: pcvr-pulse 2.4s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .pcvr-drift, .pcvr-sheen, .pcvr-pulse { animation: none !important; }
        }
      `}</style>

      <div
        aria-hidden="true"
        className="pointer-events-none absolute -top-40 left-1/2 h-96 w-[52rem] -translate-x-1/2 rounded-full bg-indigo-400/15 blur-3xl dark:bg-indigo-500/10"
      />

      <div className="relative mx-auto w-full max-w-5xl px-4 sm:px-6">
        <motion.div
          initial={reduced ? false : { opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
          className="overflow-hidden rounded-3xl bg-white shadow-xl shadow-slate-900/5 ring-1 ring-slate-900/10 dark:bg-zinc-950 dark:shadow-black/40 dark:ring-white/10"
        >
          {/* Cover */}
          <div className="relative h-56 w-full overflow-hidden bg-slate-200 sm:h-72 lg:h-80 dark:bg-zinc-900">
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src={active.src}
              alt={active.alt}
              loading="lazy"
              draggable={false}
              style={{ objectPosition: `50% ${activeFocal}%` }}
              className={`absolute inset-0 h-full w-full select-none object-cover transition-[object-position] duration-200 ease-out ${
                editing || reduced ? "scale-[1.06]" : "pcvr-drift"
              }`}
            />

            <div
              aria-hidden="true"
              className="absolute inset-0 bg-gradient-to-t from-black/65 via-black/10 to-black/25"
            />

            <div className="absolute right-3 top-3 flex items-center gap-2 sm:right-4 sm:top-4">
              <button
                ref={editButtonRef}
                type="button"
                onClick={() => (editing ? cancelEditor() : openEditor())}
                aria-expanded={editing}
                aria-controls={panelId}
                className={`inline-flex items-center gap-2 rounded-full border border-white/25 bg-black/45 px-3.5 py-2 text-xs font-medium text-white backdrop-blur-md transition hover:bg-black/65 sm:text-sm ${ring} focus-visible:ring-offset-black/40`}
              >
                {editing ? <CloseIcon /> : <CameraIcon />}
                {editing ? "Close editor" : "Edit cover"}
              </button>
            </div>

            <div className="absolute bottom-3 right-3 hidden max-w-[15rem] sm:bottom-4 sm:right-4 sm:block">
              <p className="truncate rounded-full border border-white/15 bg-black/40 px-3 py-1.5 text-[11px] font-medium text-white/90 backdrop-blur-md">
                {active.label}
                <span className="text-white/50"> · {active.place}</span>
              </p>
            </div>
          </div>

          {/* Cover editor */}
          <AnimatePresence initial={false}>
            {editing ? (
              <motion.div
                key="editor"
                id={panelId}
                onKeyDown={onPanelKeyDown}
                initial={reduced ? { opacity: 0 } : { height: 0, opacity: 0 }}
                animate={reduced ? { opacity: 1 } : { height: "auto", opacity: 1 }}
                exit={reduced ? { opacity: 0 } : { height: 0, opacity: 0 }}
                transition={{ duration: 0.32, ease: [0.22, 1, 0.36, 1] }}
                className="overflow-hidden border-b border-slate-200 bg-slate-50 dark:border-white/10 dark:bg-zinc-900/70"
              >
                <div className="flex flex-col gap-5 px-5 py-5 sm:px-8 sm:py-6">
                  <div className="flex flex-wrap items-center justify-between gap-3">
                    <div>
                      <h3 className="text-sm font-semibold text-slate-900 dark:text-zinc-100">Cover photo</h3>
                      <p className="mt-0.5 text-xs text-slate-500 dark:text-zinc-400">
                        Pick a shot, then drag the slider to move the crop up or down. Escape discards.
                      </p>
                    </div>
                    <div className="flex items-center gap-2">
                      <button
                        type="button"
                        onClick={cancelEditor}
                        className={`rounded-full border border-slate-300 bg-white px-4 py-2 text-xs font-medium text-slate-700 transition hover:bg-slate-100 dark:border-white/15 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-900 ${ring}`}
                      >
                        Discard
                      </button>
                      <button
                        type="button"
                        onClick={saveEditor}
                        className={`inline-flex items-center gap-1.5 rounded-full bg-indigo-600 px-4 py-2 text-xs font-semibold text-white transition hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400 ${ring}`}
                      >
                        <CheckIcon />
                        Save cover
                      </button>
                    </div>
                  </div>

                  <div
                    role="radiogroup"
                    aria-label="Choose a cover photo"
                    onKeyDown={onThumbKeyDown}
                    className="-mx-1 flex gap-3 overflow-x-auto px-1 pb-1"
                  >
                    {COVERS.map((cover, index) => {
                      const selected = cover.id === draftCover;
                      return (
                        <button
                          key={cover.id}
                          ref={(el) => {
                            thumbRefs.current[index] = el;
                          }}
                          type="button"
                          role="radio"
                          aria-checked={selected}
                          tabIndex={selected ? 0 : -1}
                          onClick={() => setDraftCover(cover.id)}
                          className={`group relative shrink-0 rounded-2xl p-1 transition ${ring} ${
                            selected
                              ? "bg-indigo-600 dark:bg-indigo-500"
                              : "bg-slate-200 hover:bg-slate-300 dark:bg-zinc-800 dark:hover:bg-zinc-700"
                          }`}
                        >
                          <span className="block overflow-hidden rounded-xl">
                            {/* eslint-disable-next-line @next/next/no-img-element */}
                            <img
                              src={cover.src}
                              alt={cover.label}
                              loading="lazy"
                              draggable={false}
                              className="h-14 w-24 select-none object-cover transition duration-300 group-hover:scale-105 sm:h-16 sm:w-28"
                            />
                          </span>
                          {selected ? (
                            <span
                              aria-hidden="true"
                              className="absolute -right-1 -top-1 grid h-5 w-5 place-items-center rounded-full bg-indigo-600 text-white ring-2 ring-slate-50 dark:bg-indigo-500 dark:ring-zinc-900"
                            >
                              <svg viewBox="0 0 24 24" fill="none" className="h-3 w-3">
                                <path
                                  d="M5 12.5l4.5 4.5L19 7"
                                  stroke="currentColor"
                                  strokeWidth="3"
                                  strokeLinecap="round"
                                  strokeLinejoin="round"
                                />
                              </svg>
                            </span>
                          ) : null}
                        </button>
                      );
                    })}
                  </div>

                  <div className="flex flex-col gap-2">
                    <div className="flex items-baseline justify-between gap-3">
                      <label htmlFor={focalId} className="text-xs font-medium text-slate-700 dark:text-zinc-300">
                        Vertical crop
                      </label>
                      <span className="font-mono text-[11px] tabular-nums text-slate-500 dark:text-zinc-400">
                        {draftFocal}% from top
                      </span>
                    </div>
                    <input
                      id={focalId}
                      type="range"
                      min={0}
                      max={100}
                      step={1}
                      value={draftFocal}
                      onChange={(event) => setDraftFocal(Number(event.target.value))}
                      aria-valuetext={`${draftFocal} percent from the top of the photo`}
                      className={`h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-300 accent-indigo-600 dark:bg-zinc-700 dark:accent-indigo-400 ${ring} focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-zinc-900`}
                    />
                  </div>
                </div>
              </motion.div>
            ) : null}
          </AnimatePresence>

          {/* Identity */}
          <div className="px-5 pb-8 sm:px-8 sm:pb-10">
            <div className="flex flex-wrap items-end justify-between gap-4">
              <div className="-mt-12 sm:-mt-14">
                <div className="relative grid h-24 w-24 place-items-center overflow-hidden rounded-3xl bg-gradient-to-br from-indigo-500 via-violet-500 to-sky-500 ring-4 ring-white sm:h-28 sm:w-28 dark:ring-zinc-950">
                  <span className="select-none text-3xl font-semibold tracking-tight text-white sm:text-4xl">
                    PR
                  </span>
                  <span
                    aria-hidden="true"
                    className="pcvr-sheen absolute inset-y-0 -left-1/3 w-1/3 bg-gradient-to-r from-transparent via-white/45 to-transparent"
                  />
                </div>
              </div>

              <div className="ml-auto flex items-center gap-2 pb-1">
                <button
                  type="button"
                  onClick={handleCopy}
                  className={`inline-flex items-center gap-2 rounded-full border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-100 dark:border-white/15 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-900 ${ring}`}
                >
                  {copied ? (
                    <span className="text-emerald-600 dark:text-emerald-400">
                      <CheckIcon />
                    </span>
                  ) : (
                    <CopyIcon />
                  )}
                  <span className="hidden sm:inline">{copied ? "Copied" : "Copy link"}</span>
                </button>

                <button
                  type="button"
                  aria-pressed={following}
                  onClick={() => setFollowing((value) => !value)}
                  className={`inline-flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-semibold transition ${ring} ${
                    following
                      ? "border border-emerald-500/40 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-400/30 dark:bg-emerald-500/10 dark:text-emerald-300 dark:hover:bg-emerald-500/20"
                      : "bg-slate-900 text-white hover:bg-slate-700 dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-200"
                  }`}
                >
                  {following ? <CheckIcon /> : <PlusIcon />}
                  {following ? "Following" : "Follow"}
                </button>
              </div>
            </div>

            <motion.div
              initial={reduced ? false : "hidden"}
              animate="shown"
              variants={{ shown: { transition: { staggerChildren: 0.07, delayChildren: 0.12 } } }}
              className="mt-5"
            >
              <motion.div
                variants={{ hidden: { opacity: 0, y: 10 }, shown: { opacity: 1, y: 0 } }}
                transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
                className="flex flex-wrap items-center gap-x-3 gap-y-2"
              >
                <h2 className="text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-zinc-50">
                  Priya Raghunathan
                </h2>
                <span
                  title="Verified maintainer"
                  className="inline-flex h-5 w-5 text-sky-500 dark:text-sky-400"
                >
                  <CheckBadgeIcon />
                  <span className="sr-only">Verified maintainer</span>
                </span>
                <span className="inline-flex items-center gap-1.5 rounded-full bg-emerald-50 px-2.5 py-1 text-xs font-medium text-emerald-700 ring-1 ring-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-400/20">
                  <span aria-hidden="true" className="pcvr-pulse h-1.5 w-1.5 rounded-full bg-emerald-500" />
                  Open to advisory work
                </span>
              </motion.div>

              <motion.p
                variants={{ hidden: { opacity: 0, y: 10 }, shown: { opacity: 1, y: 0 } }}
                transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
                className="mt-1 text-sm text-slate-500 dark:text-zinc-400"
              >
                <span className="font-mono">@priyaships</span>
                <span className="px-2 text-slate-300 dark:text-zinc-700">/</span>
                Staff Design Engineer at Meridian Labs
              </motion.p>

              <motion.p
                variants={{ hidden: { opacity: 0, y: 10 }, shown: { opacity: 1, y: 0 } }}
                transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
                className="mt-5 max-w-2xl text-base leading-relaxed text-slate-600 dark:text-zinc-300"
              >
                I build the parts of an interface that have to survive real users — tables that stay smooth at
                40,000 rows, forms that recover when the network dies mid-submit, focus that lands exactly where
                you expected it to. This year I&apos;m rebuilding Meridian&apos;s editor surface. Six years on infra
                tooling before that, which is why I&apos;m insufferable about latency.
              </motion.p>

              <motion.ul
                variants={{ hidden: { opacity: 0, y: 10 }, shown: { opacity: 1, y: 0 } }}
                transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
                className="mt-5 flex flex-wrap items-center gap-x-5 gap-y-2 text-sm text-slate-500 dark:text-zinc-400"
              >
                <li className="inline-flex items-center gap-1.5">
                  <PinIcon />
                  Rotterdam, NL
                </li>
                <li className="inline-flex items-center gap-1.5">
                  <LinkIcon />
                  <a
                    href="https://priya.build"
                    target="_blank"
                    rel="noopener"
                    className={`rounded-sm text-indigo-600 underline-offset-4 hover:underline dark:text-indigo-400 ${ring}`}
                  >
                    priya.build
                  </a>
                </li>
                <li className="inline-flex items-center gap-1.5">
                  <CalendarIcon />
                  Joined March 2019
                </li>
              </motion.ul>

              <motion.ul
                variants={{ hidden: { opacity: 0, y: 10 }, shown: { opacity: 1, y: 0 } }}
                transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
                className="mt-4 flex flex-wrap gap-2"
              >
                {CHIPS.map((chip) => (
                  <li
                    key={chip}
                    className="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-600 dark:bg-zinc-900 dark:text-zinc-300"
                  >
                    {chip}
                  </li>
                ))}
              </motion.ul>

              <motion.dl
                variants={{ hidden: { opacity: 0, y: 10 }, shown: { opacity: 1, y: 0 } }}
                transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
                className="mt-7 grid grid-cols-1 gap-px overflow-hidden rounded-2xl bg-slate-200 ring-1 ring-slate-200 sm:grid-cols-3 dark:bg-white/10 dark:ring-white/10"
              >
                {STATS.map((stat, index) => (
                  <div key={stat.label} className="bg-white px-4 py-4 dark:bg-zinc-950">
                    <dt className="text-xs font-medium uppercase tracking-wider text-slate-400 dark:text-zinc-500">
                      {stat.label}
                    </dt>
                    <dd
                      aria-live={index === 0 ? "polite" : undefined}
                      className="mt-1 text-xl font-semibold tabular-nums text-slate-900 dark:text-zinc-50"
                    >
                      {index === 0 ? followerLabel : stat.value}
                    </dd>
                    <dd className="mt-0.5 text-xs text-slate-500 dark:text-zinc-400">{stat.hint}</dd>
                  </div>
                ))}
              </motion.dl>
            </motion.div>
          </div>
        </motion.div>

        <p role="status" aria-live="polite" className="sr-only">
          {status}
        </p>
      </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 →