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

Avis

Hover Cards Flip — React Hover-cards Section — Incubator