Retour au catalogue
Blog Card Stack
Articles empiles avec rotation alternee. Au hover, les cards se deplient en eventail avec animation fluide.
blogcomplex Both Responsive a11y
playfulboldagencysaasuniversalcentered
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Calendar, ArrowUpRight } from "lucide-react";
interface Article {
title: string;
excerpt: string;
date: string;
tag: string;
image: string;
url: string;
}
interface BlogCardStackProps {
heading?: string;
subtitle?: string;
articles?: Article[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
export default function BlogCardStack({
heading = "Nos derniers articles",
subtitle = "Explorez nos reflexions",
articles = [],
}: BlogCardStackProps) {
const [isExpanded, setIsExpanded] = useState(false);
const items = articles.slice(0, 5);
return (
<section
style={{
padding: "var(--section-padding-y) 0",
background: "var(--color-background)",
overflow: "hidden",
}}
>
<div
style={{
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
}}
>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: EASE }}
style={{ textAlign: "center", marginBottom: "4rem" }}
>
<p style={{ fontSize: "0.8125rem", fontWeight: 600, color: "var(--color-accent)", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: "0.75rem" }}>
{subtitle}
</p>
<h2 style={{ fontFamily: "var(--font-serif)", fontSize: "clamp(2rem, 4vw, 3.25rem)", fontWeight: 400, fontStyle: "italic", color: "var(--color-foreground)", lineHeight: 1.15 }}>
{heading}
</h2>
</motion.div>
<div
onMouseEnter={() => setIsExpanded(true)}
onMouseLeave={() => setIsExpanded(false)}
style={{ position: "relative", height: "420px", maxWidth: "640px", margin: "0 auto", cursor: "pointer" }}
>
<AnimatePresence>
{items.map((article, i) => {
const mid = (items.length - 1) / 2;
const stackRotate = (i - mid) * 3;
const fanRotate = (i - mid) * 8;
const fanX = (i - mid) * 120;
return (
<motion.a
key={article.title}
href={article.url}
initial={{ opacity: 0, y: 40, rotate: 0 }}
whileInView={{ opacity: 1, y: 0, rotate: stackRotate }}
viewport={{ once: true }}
animate={{
rotate: isExpanded ? fanRotate : stackRotate,
x: isExpanded ? fanX : 0,
scale: isExpanded ? 0.88 : 1 - i * 0.02,
zIndex: items.length - i,
}}
transition={{ duration: 0.5, delay: i * 0.05, ease: EASE }}
whileHover={{ y: -8 }}
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
borderRadius: "var(--radius-xl)",
overflow: "hidden",
textDecoration: "none",
boxShadow: `0 ${4 + i * 6}px ${20 + i * 10}px rgba(0,0,0,${0.12 + i * 0.04})`,
background: "var(--color-background-card)",
border: "1px solid var(--color-border)",
}}
>
<div
style={{
position: "absolute",
inset: 0,
backgroundImage: `url(${article.image})`,
backgroundSize: "cover",
backgroundPosition: "center",
opacity: 0.15,
}}
/>
<div style={{ position: "relative", padding: "2rem", zIndex: 1 }}>
<span style={{ display: "inline-block", padding: "0.25rem 0.75rem", borderRadius: "var(--radius-full)", background: "var(--color-accent)", fontSize: "0.75rem", fontWeight: 600, color: "var(--color-foreground)", marginBottom: "0.75rem" }}>
{article.tag}
</span>
<h3 style={{ fontFamily: "var(--font-sans)", fontSize: "1.25rem", fontWeight: 600, color: "var(--color-foreground)", lineHeight: 1.3, marginBottom: "0.5rem" }}>
{article.title}
<ArrowUpRight style={{ display: "inline", width: 16, height: 16, marginLeft: 6, opacity: 0.5 }} />
</h3>
<p style={{ fontSize: "0.875rem", color: "var(--color-foreground-muted)", lineHeight: 1.5, marginBottom: "0.75rem" }}>
{article.excerpt}
</p>
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: "0.75rem", color: "var(--color-foreground-light)" }}>
<Calendar style={{ width: 12, height: 12 }} /> {article.date}
</span>
</div>
</motion.a>
);
})}
</AnimatePresence>
</div>
</div>
</section>
);
}