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>
);
}