How to build a fan card effect with GSAP and Elementor on scrolltrigger

SHARE

SIGN UP FOR THE NEWSLETTER

Your subscription could not be saved. Please try again.
Your subscription has been successful.


Code:

Javascript

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script src="https://unpkg.com/lenis@1.1.18/dist/lenis.min.js"></script>
<script src="./script.js"></script>
    
<script>
  document.addEventListener("DOMContentLoaded", () => {
  gsap.registerPlugin(ScrollTrigger);

  const lenis = new Lenis();
  lenis.on("scroll", ScrollTrigger.update);
  gsap.ticker.add((time) => {
    lenis.raf(time * 1000);
  });
  gsap.ticker.lagSmoothing(0);

  const stickySection = document.querySelector(".steps");
  const stickyHeight = window.innerHeight * 7;
  const cards = document.querySelectorAll(".card");
  const countContainer = document.querySelector(".count-container");
  const totalCards = cards.length;

  ScrollTrigger.create({
    trigger: stickySection,
    start: "top top",
    end: `+=${stickyHeight}px`,
    pin: true,
    pinSpacing: true,
    onUpdate: (self) => {
      positionCards(self.progress);
    },
  });

  const getRadius = () => {
    return window.innerWidth < 900
      ? window.innerWidth * 7.5
      : window.innerWidth * 2.5;
  };

  const arcAngle = Math.PI * 0.4;
  const startAngle = Math.PI / 2 - arcAngle / 2;

  function positionCards(progress = 0) {
    const radius = getRadius();
    const totalTravel = 1 + totalCards / 7.5;
    const adjustedProgress = (progress * totalTravel - 1) * 0.75;

    cards.forEach((card, i) => {
      const normalizedProgress = (totalCards - 1 - i) / totalCards;
      const cardProgress = normalizedProgress + adjustedProgress;
      const angle = startAngle + arcAngle * cardProgress;

      const x = Math.cos(angle) * radius;
      const y = Math.sin(angle) * radius;
      const rotation = (angle - Math.PI / 2) * (180 / Math.PI);

      gsap.set(card, {
        x: x,
        y: -y + radius,
        rotation: -rotation,
        transformOrigin: "center center",
      });
    });
  }

  positionCards(0);

  let currentCardIndex = 0;

  const options = {
    root: null,
    rootMargin: "0% 0%",
    threshold: 0.5,
  };

  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        lastScrollY = window.scrollY;

        let cardIndex = Array.from(cards).indexOf(entry.target);

        currentCardIndex = cardIndex;
        console.log(currentCardIndex);

        const targetY = 150 - currentCardIndex * 150;
        gsap.to(countContainer, {
          y: targetY,
          duration: 0.3,
          ease: "power1.out",
          overwrite: true,
        });
      }
    });
  }, options);

  cards.forEach((card) => {
    observer.observe(card);
  });

  window.addEventListener("resize", () => positionCards(0));
});

</script>

CSS

section {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}


.cards {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 550vw;
  height: 600px;
  will-change: transform;
}

.card {
  position: absolute;
  width: 500px;
  height: 50px;
  left: 50%;
  top: 130%;
  transform-origin: center center;
  margin-left: -150px;
  display: flex;
  flex-direction: column;
  gap: 1em;
  will-change: transform;
}



.card-content {
  width: 100%;
  height: 60px;
}



.step-counter {
  position: absolute;
  display: flex;
  flex-direction: column;
  margin: 2em;
}

.counter-title,
.count {
  position: relative;
  /*width: 1200px;*/
  height: 100px;
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
  overflow: hidden;
}

.count {
  position: relative;
  top: -30px;
}

.count-container {
  position: relative;
  transform: translateY(150px);
  will-change: transform;
}

.step-counter h2 {
  width: 100%;
  position: relative;
  color: #fff;
  text-transform: uppercase;
 
  font-weight: 900;
  font-size: 90px;
  line-height: 1;
  letter-spacing: -0.04em;
  will-change: transform;
}

.empty {
  opacity: 0;
}

.lenis.lenis-smooth {
  scroll-behavior: auto !important;
}

.lenis.lenis-smooth [data-lenis-prevent] {
  overscroll-behavior: contain;
}

.lenis.lenis-stopped {
  overflow: clip;
}

.lenis.lenis-smooth iframe {
  pointer-events: none;
}

@media (max-width: 900px) {
  .counter-title {
    height: 30px;
  }

  .counter-title h2 {
    font-size: 30px;
  }

  .count {
    top: 0px;
    left: -10px;
  }

  .cards {
    top: 27.5%;
  }

  .card {
    width: 375px;
    height: 500px;
  }
}



Video:

SIGN UP FOR THE NEWSLETTER

Your subscription could not be saved. Please try again.
Your subscription has been successful.