Web InnoventixFreeCode

Profile Skeleton

Original · free

profile header skeleton

byWeb InnoventixReact + Tailwind
skelprofileskeletons
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/skel-profile.json
skel-profile.tsx
"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

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 →