Retour au catalogue
Hover Cards Flip
Grille de cartes avec effet flip 3D au hover, revelant le contenu arriere avec background accent.
hover-cardsmedium Both Responsive a11y
playfulbolduniversalagencysaasgrid
Theme
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import * as LucideIcons from "lucide-react";
import React from "react";
interface FlipCardItem {
id: string;
icon?: string;
frontTitle: string;
frontDescription: string;
backTitle: string;
backDescription: string;
}
interface HoverCardsFlipProps {
badge?: string;
title?: string;
subtitle?: string;
cards: FlipCardItem[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
function getIcon(name?: string) {
if (!name) return null;
return (LucideIcons as unknown as Record<string, React.ElementType>)[name] || null;
}
function FlipCard({ card, index }: { card: FlipCardItem; index: number }) {
const [isFlipped, setIsFlipped] = useState(false);
const Icon = getIcon(card.icon);
return (
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ delay: index * 0.1, duration: 0.5, ease: EASE }}
onMouseEnter={() => setIsFlipped(true)}
onMouseLeave={() => setIsFlipped(false)}
style={{
perspective: "1000px",
minHeight: "280px",
cursor: "pointer",
}}
>
<motion.div
animate={{ rotateY: isFlipped ? 180 : 0 }}
transition={{ duration: 0.5, ease: EASE }}
style={{
position: "relative",
width: "100%",
height: "100%",
minHeight: "280px",
transformStyle: "preserve-3d",
}}
>
{/* Front */}
<div
style={{
position: "absolute",
inset: 0,
backfaceVisibility: "hidden",
borderRadius: "var(--radius-xl, 1.5rem)",
border: "1px solid var(--color-border)",
background: "var(--color-background-card)",
padding: "2rem",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
}}
>
{Icon && (
<div
style={{
width: "48px",
height: "48px",
borderRadius: "var(--radius-md, 0.75rem)",
background: "color-mix(in srgb, var(--color-accent) 12%, transparent)",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: "1.25rem",
}}
>
<Icon style={{ width: 24, height: 24, color: "var(--color-accent)" }} />
</div>
)}
<h3
style={{
fontSize: "1.125rem",
fontWeight: 700,
color: "var(--color-foreground)",
marginBottom: "0.5rem",
}}
>
{card.frontTitle}
</h3>
<p style={{ fontSize: "0.875rem", lineHeight: 1.6, color: "var(--color-foreground-muted)" }}>
{card.frontDescription}
</p>
</div>
{/* Back */}
<div
style={{
position: "absolute",
inset: 0,
backfaceVisibility: "hidden",
transform: "rotateY(180deg)",
borderRadius: "var(--radius-xl, 1.5rem)",
background: "var(--color-accent)",
padding: "2rem",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
}}
>
<h3
style={{
fontSize: "1.125rem",
fontWeight: 700,
color: "var(--color-foreground)",
marginBottom: "0.75rem",
}}
>
{card.backTitle}
</h3>
<p style={{ fontSize: "0.875rem", lineHeight: 1.6, color: "var(--color-foreground)" }}>
{card.backDescription}
</p>
</div>
</motion.div>
</motion.div>
);
}
export default function HoverCardsFlip({
badge = "Services",
title = "Ce que nous proposons",
subtitle = "Survolez pour en decouvrir plus.",
cards = [],
}: HoverCardsFlipProps) {
return (
<section
style={{
padding: "var(--section-padding-y, 6rem) 0",
background: "var(--color-background)",
}}
>
<div
style={{
maxWidth: "var(--container-max-width, 1200px)",
margin: "0 auto",
padding: "0 var(--container-padding-x, 1.5rem)",
}}
>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.5, ease: EASE }}
style={{ textAlign: "center", maxWidth: "600px", margin: "0 auto 3.5rem" }}
>
{badge && (
<span
style={{
display: "inline-block",
fontSize: "0.75rem",
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.08em",
color: "var(--color-accent)",
marginBottom: "0.75rem",
}}
>
{badge}
</span>
)}
<h2
style={{
fontFamily: "var(--font-sans)",
fontSize: "clamp(1.75rem, 3vw, 2.75rem)",
fontWeight: 700,
lineHeight: 1.15,
letterSpacing: "-0.02em",
color: "var(--color-foreground)",
marginBottom: "1rem",
}}
>
{title}
</h2>
<p style={{ fontSize: "1rem", lineHeight: 1.7, color: "var(--color-foreground-muted)" }}>
{subtitle}
</p>
</motion.div>
<div
style={{ display: "grid", gap: "1.5rem" }}
className="grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
>
{cards.map((card, i) => (
<FlipCard key={card.id} card={card} index={i} />
))}
</div>
</div>
</section>
);
}