Retour au catalogue

Process Card Stack

Cards de process empilees en position sticky. Chaque card pousse la precedente au scroll. Numerotation geante en background. Style Apple.

processcomplex Both Responsive a11y
boldcorporateuniversalsaasagencystacked
Theme
"use client";

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

interface Step { number: string; title: string; description: string }
interface ProcessCardStackProps { title?: string; subtitle?: string; steps?: Step[] }

const E = [0.16, 1, 0.3, 1] as const;
const HEAD: React.CSSProperties = { fontFamily: "var(--font-sans)", fontSize: "clamp(2rem, 4vw, 3rem)", fontWeight: 700, lineHeight: 1.1, letterSpacing: "-0.02em", color: "var(--color-foreground)", marginBottom: "1rem" };

export default function ProcessCardStack({ title = "Comment ca marche", subtitle = "Un processus simple en quatre etapes.", steps = [] }: ProcessCardStackProps) {
  return (
    <section style={{ paddingTop: "var(--section-padding-y-lg)", paddingBottom: 0, background: "var(--color-background)" }}>
      <div style={{ maxWidth: "var(--container-max-width)", margin: "0 auto", padding: "0 var(--container-padding-x)" }}>
        <div style={{ textAlign: "center", marginBottom: "3rem" }}>
          <motion.h2 initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.6, ease: E }} style={HEAD}>{title}</motion.h2>
          <motion.p initial={{ opacity: 0, y: 12 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.1, ease: E }} style={{ fontSize: "1.0625rem", lineHeight: 1.7, color: "var(--color-foreground-muted)", maxWidth: "480px", margin: "0 auto" }}>{subtitle}</motion.p>
        </div>
      </div>
      <div style={{ position: "relative" }}>
        {steps.map((step, i) => <StickyCard key={i} step={step} index={i} total={steps.length} />)}
      </div>
    </section>
  );
}

function StickyCard({ step, index, total }: { step: Step; index: number; total: number }) {
  const ref = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({ target: ref, offset: ["start start", "end start"] });
  const scale = useTransform(scrollYProgress, [0, 1], [1, 0.92]);
  const opacity = useTransform(scrollYProgress, [0.6, 1], [1, 0.4]);
  const isLast = index >= total - 1;

  return (
    <div ref={ref} style={{ height: isLast ? "auto" : "100vh", position: "relative" }}>
      <motion.div style={{ position: "sticky", top: `calc(4rem + ${index * 2}rem)`, scale: isLast ? 1 : scale, opacity: isLast ? 1 : opacity, zIndex: index }}>
        <div style={{ maxWidth: "var(--container-max-width)", margin: "0 auto", padding: "0 var(--container-padding-x)" }}>
          <div style={{ position: "relative", overflow: "hidden", borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)", background: "var(--color-background-card)", padding: "3rem 2.5rem", minHeight: "280px", display: "flex", alignItems: "center" }}>
            <div aria-hidden style={{ position: "absolute", right: "2rem", top: "50%", transform: "translateY(-50%)", fontFamily: "var(--font-sans)", fontSize: "clamp(8rem, 15vw, 14rem)", fontWeight: 900, lineHeight: 1, color: "var(--color-accent)", opacity: 0.06, pointerEvents: "none", userSelect: "none" }}>{step.number}</div>
            <div style={{ position: "relative", zIndex: 1, maxWidth: "520px" }}>
              <span style={{ display: "inline-block", fontFamily: "var(--font-sans)", fontSize: "0.75rem", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.1em", color: "var(--color-accent)", marginBottom: "1rem" }}>Etape {step.number}</span>
              <h3 style={{ fontFamily: "var(--font-sans)", fontSize: "clamp(1.5rem, 3vw, 2rem)", fontWeight: 700, lineHeight: 1.15, color: "var(--color-foreground)", marginBottom: "1rem" }}>{step.title}</h3>
              <p style={{ fontSize: "1.0625rem", lineHeight: 1.7, color: "var(--color-foreground-muted)" }}>{step.description}</p>
            </div>
          </div>
        </div>
      </motion.div>
    </div>
  );
}

Avis

Process Card Stack — React Process Section — Incubator