Retour au catalogue

About Cards Stack

Histoire racontee via des cartes empilees qui s'etalent au scroll.

aboutcomplex Both Responsive a11y
playfulboldagencysaasuniversalstacked
Theme
"use client";

import React, { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";

interface Card {
  id: string;
  title: string;
  text: string;
  image?: string;
  accent?: boolean;
}

interface AboutCardsStackProps {
  badge?: string;
  title?: string;
  cards?: Card[];
}

function StackCard({ card, index, total }: { card: Card; index: number; total: number }) {
  const ref = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "start 0.3"] });
  const y = useTransform(scrollYProgress, [0, 1], [100, 0]);
  const rotate = useTransform(scrollYProgress, [0, 1], [index % 2 === 0 ? 3 : -3, 0]);
  const scale = useTransform(scrollYProgress, [0, 1], [0.9, 1]);

  return (
    <motion.div
      ref={ref}
      style={{
        y, rotate, scale, zIndex: total - index,
        ...(card.accent
          ? { backgroundColor: "var(--color-background-dark)", borderColor: "var(--color-accent)" }
          : { backgroundColor: "var(--color-background-card)", borderColor: "var(--color-border)" }
        ),
      }}
      className="rounded-[var(--radius-xl,1.5rem)] border overflow-hidden shadow-lg"
    >
      <div className="grid grid-cols-1 md:grid-cols-2 gap-0">
        {card.image && (
          <div className="aspect-[3/2] md:aspect-auto" style={{ backgroundColor: "var(--color-background-alt)", backgroundImage: `url(${card.image})`, backgroundSize: "cover", backgroundPosition: "center" }} />
        )}
        <div className="p-8 md:p-10 flex flex-col justify-center">
          <span className="text-xs font-bold tracking-wider" style={{ color: "var(--color-accent)" }}>0{index + 1}</span>
          <h3 className="mt-2 text-xl font-bold md:text-2xl" style={{ color: card.accent ? "var(--color-foreground)" : "var(--color-foreground)" }}>{card.title}</h3>
          <p className="mt-3 text-sm leading-relaxed" style={{ color: "var(--color-foreground-muted)" }}>{card.text}</p>
        </div>
      </div>
    </motion.div>
  );
}

export default function AboutCardsStack({ badge, title, cards = [] }: AboutCardsStackProps) {
  return (
    <section className="py-[var(--section-padding-y,6rem)]" style={{ backgroundColor: "var(--color-background)" }}>
      <div className="mx-auto max-w-4xl px-[var(--container-padding-x,1.5rem)]">
        <motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5 }} className="text-center max-w-2xl mx-auto mb-16">
          {badge && <span className="inline-block text-xs font-medium tracking-wider uppercase px-3 py-1 rounded-full border" style={{ color: "var(--color-accent)", borderColor: "var(--color-border)" }}>{badge}</span>}
          {title && <h2 className="mt-4 text-3xl font-bold tracking-tight md:text-4xl" style={{ color: "var(--color-foreground)" }}>{title}</h2>}
        </motion.div>
        <div className="space-y-8">
          {cards.map((card, i) => <StackCard key={card.id} card={card} index={i} total={cards.length} />)}
        </div>
      </div>
    </section>
  );
}

Avis

About Cards Stack — React About Section — Incubator