Web InnoventixFreeCode

Team Card

Original · free

A team member card with photo, role and social links.

byWeb InnoventixReact + Tailwind
cardteamcards
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/card-team.json
card-team.tsx
"use client";

import { useState } from "react";

type Social = {
  label: string;
  href: string;
  glyph: "x" | "linkedin" | "github" | "dribbble" | "mail";
};

type Member = {
  name: string;
  role: string;
  team: string;
  location: string;
  img: string;
  bio: string;
  status: "online" | "away" | "focus";
  socials: Social[];
};

const MEMBERS: Member[] = [
  {
    name: "Priya Nair",
    role: "Principal Design Engineer",
    team: "Design Systems",
    location: "Bengaluru, IN",
    img: "/img/gallery/g07.webp",
    bio: "Builds the component library the whole product runs on. Ex-Figma, obsessive about focus states.",
    status: "online",
    socials: [
      { label: "Priya on X", href: "#", glyph: "x" },
      { label: "Priya on LinkedIn", href: "#", glyph: "linkedin" },
      { label: "Priya on Dribbble", href: "#", glyph: "dribbble" },
      { label: "Email Priya", href: "#", glyph: "mail" },
    ],
  },
  {
    name: "Marcus Reyes",
    role: "Staff Backend Engineer",
    team: "Platform",
    location: "Austin, US",
    img: "/img/gallery/g12.webp",
    bio: "Owns the billing pipeline and the parts of the graph nobody else wants to touch.",
    status: "focus",
    socials: [
      { label: "Marcus on GitHub", href: "#", glyph: "github" },
      { label: "Marcus on LinkedIn", href: "#", glyph: "linkedin" },
      { label: "Email Marcus", href: "#", glyph: "mail" },
    ],
  },
  {
    name: "Sofia Lindqvist",
    role: "Head of Product",
    team: "Leadership",
    location: "Stockholm, SE",
    img: "/img/gallery/g23.webp",
    bio: "Turns a wall of customer calls into a roadmap. Believes in shipping small, often, and on purpose.",
    status: "away",
    socials: [
      { label: "Sofia on X", href: "#", glyph: "x" },
      { label: "Sofia on LinkedIn", href: "#", glyph: "linkedin" },
      { label: "Email Sofia", href: "#", glyph: "mail" },
    ],
  },
];

const STATUS: Record<
  Member["status"],
  { dot: string; ring: string; text: string; label: string }
> = {
  online: {
    dot: "bg-emerald-500",
    ring: "ring-emerald-500/30",
    text: "text-emerald-600 dark:text-emerald-400",
    label: "Available",
  },
  focus: {
    dot: "bg-violet-500",
    ring: "ring-violet-500/30",
    text: "text-violet-600 dark:text-violet-400",
    label: "In focus",
  },
  away: {
    dot: "bg-amber-500",
    ring: "ring-amber-500/30",
    text: "text-amber-600 dark:text-amber-400",
    label: "Away",
  },
};

function Glyph({ name }: { name: Social["glyph"] }) {
  const common = "h-4 w-4";
  switch (name) {
    case "x":
      return (
        <svg viewBox="0 0 24 24" fill="currentColor" className={common} aria-hidden="true">
          <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.657l-5.214-6.817-5.966 6.817H1.68l7.73-8.835L1.254 2.25h6.826l4.713 6.231 5.451-6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77Z" />
        </svg>
      );
    case "linkedin":
      return (
        <svg viewBox="0 0 24 24" fill="currentColor" className={common} aria-hidden="true">
          <path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.42v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28ZM5.34 7.43a2.07 2.07 0 1 1 0-4.14 2.07 2.07 0 0 1 0 4.14ZM7.12 20.45H3.55V9h3.57v11.45ZM22.23 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.22.79 24 1.77 24h20.46c.98 0 1.77-.78 1.77-1.73V1.73C24 .77 23.21 0 22.23 0Z" />
        </svg>
      );
    case "github":
      return (
        <svg viewBox="0 0 24 24" fill="currentColor" className={common} aria-hidden="true">
          <path d="M12 .5A11.5 11.5 0 0 0 .5 12a11.5 11.5 0 0 0 7.86 10.92c.58.11.79-.25.79-.56v-2c-3.2.7-3.88-1.37-3.88-1.37-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.2 1.77 1.2 1.03 1.77 2.71 1.26 3.37.96.1-.75.4-1.26.73-1.55-2.56-.29-5.25-1.28-5.25-5.7 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.79 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.81 1.19 1.84 1.19 3.1 0 4.43-2.7 5.4-5.27 5.69.41.36.78 1.06.78 2.14v3.17c0 .31.21.68.8.56A11.5 11.5 0 0 0 23.5 12 11.5 11.5 0 0 0 12 .5Z" />
        </svg>
      );
    case "dribbble":
      return (
        <svg viewBox="0 0 24 24" fill="currentColor" className={common} aria-hidden="true">
          <path d="M12 0a12 12 0 1 0 0 24 12 12 0 0 0 0-24Zm7.93 5.53a10.16 10.16 0 0 1 2.3 6.36c-.34-.07-3.72-.76-7.12-.33-.07-.17-.14-.35-.22-.53-.21-.5-.44-1-.68-1.48 3.77-1.54 5.48-3.75 5.72-4.02ZM12 1.79c2.57 0 4.92.96 6.71 2.55-.2.28-1.74 2.35-5.38 3.72a48.5 48.5 0 0 0-3.87-6.03A10.3 10.3 0 0 1 12 1.79ZM7.5 2.72a56 56 0 0 1 3.83 5.95c-4.83 1.28-9.1 1.26-9.56 1.26A10.24 10.24 0 0 1 7.5 2.72ZM1.77 12.01v-.31c.44.01 5.46.07 10.62-1.48.3.58.58 1.17.83 1.76l-.4.11c-5.34 1.72-8.18 6.43-8.42 6.83a10.2 10.2 0 0 1-2.63-6.91Zm10.23 10.2c-2.3 0-4.42-.77-6.11-2.07.19-.39 2.3-4.47 8.15-6.51l.07-.02a42.7 42.7 0 0 1 2.19 7.78 10.16 10.16 0 0 1-4.3.82Zm6.03-1.78c-.12-.72-.66-3.64-1.99-7.34 3.2-.51 6.01.32 6.36.44a10.22 10.22 0 0 1-4.37 6.9Z" />
        </svg>
      );
    case "mail":
      return (
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className={common} aria-hidden="true">
          <rect x="3" y="5" width="18" height="14" rx="2.5" />
          <path d="m4 7 8 6 8-6" strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      );
  }
}

function SocialButton({ social }: { social: Social }) {
  return (
    <a
      href={social.href}
      target="_blank"
      rel="noopener"
      aria-label={social.label}
      className="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 transition-[transform,color,background-color,border-color] duration-200 hover:-translate-y-0.5 hover:border-indigo-300 hover:bg-indigo-50 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 active:translate-y-0 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:border-indigo-500/50 dark:hover:bg-indigo-500/10 dark:hover:text-indigo-300 dark:focus-visible:ring-offset-zinc-900"
    >
      <Glyph name={social.glyph} />
    </a>
  );
}

function TeamCard({ member }: { member: Member }) {
  const [followed, setFollowed] = useState(false);
  const s = STATUS[member.status];

  return (
    <article className="group relative flex w-full flex-col overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm transition-shadow duration-300 hover:shadow-xl hover:shadow-slate-900/5 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:shadow-black/40">
      <div className="pointer-events-none absolute inset-x-0 top-0 h-28 bg-gradient-to-b from-indigo-500/10 via-violet-500/5 to-transparent dark:from-indigo-500/15 dark:via-violet-500/[0.07]" />

      <div className="relative flex flex-col items-center px-6 pt-8">
        <div className={`relative rounded-full p-1 ring-2 ${s.ring}`}>
          <div className="overflow-hidden rounded-full">
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src={member.img}
              alt={`Portrait of ${member.name}`}
              loading="lazy"
              draggable={false}
              className="ct-avatar h-24 w-24 rounded-full object-cover"
            />
          </div>
          <span
            className={`absolute bottom-1 right-1 h-4 w-4 rounded-full border-2 border-white ${s.dot} dark:border-zinc-900`}
            aria-hidden="true"
          />
        </div>

        <div className="mt-4 flex flex-col items-center text-center">
          <h3 className="text-lg font-semibold tracking-tight text-slate-900 dark:text-white">
            {member.name}
          </h3>
          <p className="mt-0.5 text-sm font-medium text-indigo-600 dark:text-indigo-400">
            {member.role}
          </p>
          <div className="mt-2 flex flex-wrap items-center justify-center gap-1.5 text-xs">
            <span className="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2.5 py-1 font-medium text-slate-600 dark:bg-zinc-800 dark:text-zinc-300">
              {member.team}
            </span>
            <span className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 font-medium ${s.text}`}>
              <span className={`h-1.5 w-1.5 rounded-full ${s.dot}`} aria-hidden="true" />
              {s.label}
            </span>
          </div>
        </div>
      </div>

      <p className="mt-4 px-6 text-center text-sm leading-relaxed text-slate-500 dark:text-zinc-400">
        {member.bio}
      </p>

      <div className="mt-4 flex items-center justify-center gap-1.5 px-6 text-xs text-slate-400 dark:text-zinc-500">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-3.5 w-3.5" aria-hidden="true">
          <path d="M12 21s-7-5.2-7-11a7 7 0 0 1 14 0c0 5.8-7 11-7 11Z" strokeLinejoin="round" />
          <circle cx="12" cy="10" r="2.5" />
        </svg>
        {member.location}
      </div>

      <div className="mt-6 flex items-center justify-between gap-3 border-t border-slate-100 px-6 py-4 dark:border-zinc-800">
        <div className="flex items-center gap-1.5">
          {member.socials.map((soc) => (
            <SocialButton key={soc.label} social={soc} />
          ))}
        </div>

        <button
          type="button"
          onClick={() => setFollowed((v) => !v)}
          aria-pressed={followed}
          className={`inline-flex items-center gap-1.5 rounded-xl px-3.5 py-2 text-sm font-semibold transition-[transform,background-color,color,border-color] duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:scale-[0.97] dark:focus-visible:ring-offset-zinc-900 ${
            followed
              ? "bg-slate-900 text-white ring-slate-900 focus-visible:ring-slate-500 dark:bg-white dark:text-zinc-900 dark:focus-visible:ring-zinc-400"
              : "border border-indigo-200 bg-indigo-50 text-indigo-700 hover:bg-indigo-100 focus-visible:ring-indigo-500 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300 dark:hover:bg-indigo-500/20"
          }`}
        >
          {followed ? (
            <>
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="h-4 w-4" aria-hidden="true">
                <path d="m5 12 4.5 4.5L19 7" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
              Following
            </>
          ) : (
            <>
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" className="h-4 w-4" aria-hidden="true">
                <path d="M12 5v14M5 12h14" strokeLinecap="round" />
              </svg>
              Follow
            </>
          )}
        </button>
      </div>
    </article>
  );
}

export default function CardTeam() {
  return (
    <section className="relative w-full bg-slate-50 px-4 py-20 dark:bg-zinc-950 sm:px-6 lg:px-8">
      <style>{`
        @keyframes ct-rise {
          from { opacity: 0; transform: translateY(14px); }
          to { opacity: 1; transform: translateY(0); }
        }
        .ct-rise { animation: ct-rise 0.6s cubic-bezier(0.22,1,0.36,1) both; }
        .group:hover .ct-avatar { transform: scale(1.04); }
        .ct-avatar { transition: transform 0.4s cubic-bezier(0.22,1,0.36,1); }
        @media (prefers-reduced-motion: reduce) {
          .ct-rise { animation: none; }
          .ct-avatar { transition: none; }
          .group:hover .ct-avatar { transform: none; }
        }
      `}</style>

      <div className="mx-auto max-w-6xl">
        <div className="mx-auto max-w-2xl text-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-500 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400">
            <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden="true" />
            The people behind the product
          </span>
          <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
            Meet the team
          </h2>
          <p className="mt-3 text-base leading-relaxed text-slate-500 dark:text-zinc-400">
            A small crew that ships. Follow along, or say hello on any channel.
          </p>
        </div>

        <div className="mt-14 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {MEMBERS.map((member, i) => (
            <div
              key={member.name}
              className="ct-rise"
              style={{ animationDelay: `${i * 90}ms` }}
            >
              <TeamCard member={member} />
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

Dependencies

None (React + Tailwind only).

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 →