Retour au catalogue

Team Card Hover

Cards equipe avec overlay au hover : bio et liens sociaux apparaissent sur la photo.

teammedium Both Responsive a11y
elegantboldagencysaasuniversalgrid
Theme
"use client";

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Linkedin, Twitter } from "lucide-react";

interface TeamMember {
  name: string;
  role: string;
  bio: string;
  linkedinUrl?: string;
  twitterUrl?: string;
}

interface TeamCardHoverProps {
  title?: string;
  subtitle?: string;
  members?: TeamMember[];
}

const EASE = [0.16, 1, 0.3, 1] as const;

function MemberCard({ member, index }: { member: TeamMember; index: number }) {
  const [hovered, setHovered] = useState(false);

  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ duration: 0.45, delay: index * 0.08, ease: EASE }}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      style={{
        position: "relative",
        borderRadius: "var(--radius-xl)",
        overflow: "hidden",
        background: "var(--color-background-alt)",
        border: "1px solid var(--color-border)",
        cursor: "pointer",
      }}
    >
      {/* Placeholder photo area */}
      <div
        style={{
          width: "100%",
          aspectRatio: "3/4",
          background: "var(--color-background-card)",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <span
          style={{
            fontSize: "3rem",
            fontWeight: 700,
            color: "var(--color-border)",
            fontFamily: "var(--font-sans)",
          }}
        >
          {member.name.charAt(0)}
        </span>
      </div>

      {/* Default label */}
      <div style={{ padding: "1rem 1.25rem" }}>
        <h3
          style={{
            fontSize: "0.9375rem",
            fontWeight: 600,
            color: "var(--color-foreground)",
          }}
        >
          {member.name}
        </h3>
        <p style={{ fontSize: "0.8125rem", color: "var(--color-foreground-muted)" }}>
          {member.role}
        </p>
      </div>

      {/* Hover overlay */}
      <AnimatePresence>
        {hovered && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.25, ease: EASE }}
            style={{
              position: "absolute",
              inset: 0,
              background: "rgba(0,0,0,0.78)",
              display: "flex",
              flexDirection: "column",
              justifyContent: "flex-end",
              padding: "1.5rem",
            }}
          >
            <h3
              style={{
                fontSize: "1rem",
                fontWeight: 600,
                color: "#fff",
                marginBottom: "0.25rem",
              }}
            >
              {member.name}
            </h3>
            <p style={{ fontSize: "0.8125rem", color: "var(--color-accent)", marginBottom: "0.75rem" }}>
              {member.role}
            </p>
            <p
              style={{
                fontSize: "0.8125rem",
                lineHeight: 1.6,
                color: "rgba(255,255,255,0.8)",
                marginBottom: "1rem",
              }}
            >
              {member.bio}
            </p>
            <div style={{ display: "flex", gap: "0.75rem" }}>
              {member.linkedinUrl && (
                <a href={member.linkedinUrl} style={{ color: "rgba(255,255,255,0.7)" }}>
                  <Linkedin style={{ width: 18, height: 18 }} />
                </a>
              )}
              {member.twitterUrl && (
                <a href={member.twitterUrl} style={{ color: "rgba(255,255,255,0.7)" }}>
                  <Twitter style={{ width: 18, height: 18 }} />
                </a>
              )}
            </div>
          </motion.div>
        )}
      </AnimatePresence>
    </motion.div>
  );
}

export default function TeamCardHover({
  title = "Notre equipe",
  subtitle = "Equipe",
  members = [],
}: TeamCardHoverProps) {
  return (
    <section
      style={{
        paddingTop: "var(--section-padding-y)",
        paddingBottom: "var(--section-padding-y)",
        background: "var(--color-background)",
      }}
    >
      <div
        style={{
          maxWidth: "var(--container-max-width)",
          margin: "0 auto",
          paddingLeft: "var(--container-padding-x)",
          paddingRight: "var(--container-padding-x)",
        }}
      >
        <motion.div
          initial={{ opacity: 0, y: 16 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ duration: 0.5, ease: EASE }}
          style={{ textAlign: "center", marginBottom: "3rem" }}
        >
          <p
            style={{
              fontSize: "0.75rem",
              fontWeight: 600,
              textTransform: "uppercase",
              letterSpacing: "0.1em",
              color: "var(--color-accent)",
              marginBottom: "0.5rem",
            }}
          >
            {subtitle}
          </p>
          <h2
            style={{
              fontFamily: "var(--font-sans)",
              fontSize: "clamp(1.5rem, 3vw, 2.25rem)",
              fontWeight: 700,
              letterSpacing: "-0.02em",
              color: "var(--color-foreground)",
            }}
          >
            {title}
          </h2>
        </motion.div>

        <div
          style={{
            display: "grid",
            gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))",
            gap: "1.25rem",
          }}
        >
          {members.map((member, i) => (
            <MemberCard key={member.name} member={member} index={i} />
          ))}
        </div>
      </div>
    </section>
  );
}

Avis

Team Card Hover — React Team Section — Incubator