Instructions

Find easy to follow instructions
GSAP Guide

All GSAP animations used in this template are collected here. On this page, you’ll find guidance on how to locate and edit them. Each code block comes with extra notes to make it easier to understand.

You can find the code in the Embed Code inside this template.

Step-1 Add library package from GSAP
<link rel="stylesheet" href="https://unpkg.com/lenis@1.3.15/dist/lenis.css" />
<script src="https://unpkg.com/lenis@1.3.15/dist/lenis.min.js"></script>
Step-2 Setting lenis for smooth scrolling
<script>
  // Initialize Lenis
  const lenis = new Lenis({ duration: 1.4 });
  // Connect to GSAP ScrollTrigger
  lenis.on('scroll', ScrollTrigger.update);
  gsap.ticker.add((time) => {
    lenis.raf(time * 1000);
  });
  gsap.ticker.lagSmoothing(0);
</script>
Step-3 Animation all heading and sub-heading

For efficiency in animating text on headings and subheadings, you can use the following GSAP script by applying the class "text-enterance" to the div block or text element of the heading, and do the same for the subheading as well.

<script>
  gsap.registerPlugin(ScrollTrigger);

  /* ================= TEXT ENTRANCE ================= */
  gsap.utils.toArray('.text-enterance').forEach((el) => {
    gsap.from(el, {
      opacity: 0,
      y: 60,
      duration: 1.2,
      ease: 'power3.out',
      scrollTrigger: {
        trigger: el,
        start: 'top 85%',
        once: true,
      },
    });
  });

  /* ================= DESC ENTRANCE ================= */
  gsap.utils.toArray('.desc-enterance').forEach((el) => {
    gsap.from(el, {
      opacity: 0,
      y: 40,
      filter: 'blur(10px)',
      duration: 1.4,
      delay: 0.2,
      ease: 'power3.out',
      scrollTrigger: {
        trigger: el,
        start: 'top 85%',
        once: true,
      },
    });
  });
</script>
Step-4 Mouse trail on background FAQ

And in this FAQ section, there is a mouse trail animation effect, featuring a gradient image element moving in the background of the section that follows your cursor.

<script>
  document.addEventListener('DOMContentLoaded', () => {
    const faqItems = document.querySelectorAll('.wrapper-faq');
    if (!faqItems.length) return;

    faqItems.forEach((trigger) => {
      const target = trigger.querySelector('.hoverglow');
      if (!target) return;

      // Initial state
      gsap.set(target, {
        opacity: 0,
        xPercent: -50,
        yPercent: -50,
        pointerEvents: 'none',
      });

      // Smooth follow
      const xTo = gsap.quickTo(target, 'x', { duration: 0.3 });
      const yTo = gsap.quickTo(target, 'y', { duration: 0.3 });

      // Move
      trigger.addEventListener('pointermove', (e) => {
        const rect = trigger.getBoundingClientRect();
        xTo(e.clientX - rect.left);
        yTo(e.clientY - rect.top);
      });

      // Enter
      trigger.addEventListener('pointerenter', (e) => {
        const rect = trigger.getBoundingClientRect();

        gsap.set(target, {
          x: e.clientX - rect.left,
          y: e.clientY - rect.top,
        });

        gsap.to(target, {
          opacity: 0.5,
          duration: 0.25,
        });
      });

      // Leave
      trigger.addEventListener('pointerleave', () => {
        gsap.to(target, {
          opacity: 0,
          duration: 0.2,
        });
      });
    });
  });
</script>
Step-5 Scramble Text Hero

Dynamic hero text with a looping scramble effect

<script>
  gsap.registerPlugin(ScrambleTextPlugin);

  document.querySelectorAll('.scramble-title').forEach((el) => {
    const originalText = el.textContent.trim();

    gsap.to(el, {
      duration: 1.2,
      scrambleText: {
        text: originalText,
        chars: '░▒▓█', // scramble characters and cursor
        speed: 0.5,
        revealDelay: 0.1,
      },
      ease: 'none',
      repeat: -1, // Repeat infinitely
      repeatDelay: 2.5, // Delay of 2.5 seconds between repeats
    });
  });
</script>
Step-6 Team section mouse event left

Then, in the team section, you need to insert the following script code to enable the mouse effect on the horizontal team wrapper.

<script>
gsap.registerPlugin(Draggable, InertiaPlugin);

const init = () => {
  const marquee = document.querySelector('[wb-data="marquee"]');
  if (!marquee) return;

  const duration = parseInt(marquee.getAttribute("duration"), 20) || 5;
  const marqueeContent = marquee.firstChild;
  if (!marqueeContent) return;

  // Clone for seamless loop
  const marqueeContentClone = marqueeContent.cloneNode(true);
  marquee.append(marqueeContentClone);

  // Make sure marquee can be grabbed
  marquee.style.cursor = "grab";

  let tween;
  let distanceToTranslate;

  const playMarquee = () => {
    let progress = tween ? tween.progress() : 0;
    tween && tween.progress(0).kill();

    const width = parseInt(
      getComputedStyle(marqueeContent).getPropertyValue("width"),
      10
    );
    const gap = parseInt(
      getComputedStyle(marqueeContent).getPropertyValue("column-gap"),
      10
    );
    distanceToTranslate = -1 * (gap + width);

    tween = gsap.fromTo(
      marquee.children,
      { x: 0 },
      { x: distanceToTranslate, duration, ease: "none", repeat: -1 }
    );
    tween.progress(progress);
  };

  playMarquee();

  // ---- PAUSE ON HOVER ----
  marquee.addEventListener("mouseenter", () => {
    if (tween) gsap.to(tween, { timeScale: 0, duration: 0.3 });
  });
  marquee.addEventListener("mouseleave", () => {
    if (tween) gsap.to(tween, { timeScale: 1, duration: 0.3 });
  });

  // ---- DRAGGABLE ----
  // A proxy element that Draggable controls; we read its x and apply it to the tween
  const proxy = document.createElement("div");
  let startProgress = 0;

  Draggable.create(proxy, {
    type: "x",
    trigger: marquee,
    inertia: true,
    onPressInit() {
      // Pause the tween while user is interacting
      gsap.killTweensOf(tween);
      tween.timeScale(0);
      marquee.style.cursor = "grabbing";
      startProgress = tween.progress();
      gsap.set(proxy, { x: 0 });
    },
    onDrag() {
      // Map drag distance to tween progress
      // Negative distanceToTranslate means dragging right should move progress backwards
      const progressDelta = this.x / distanceToTranslate;
      let newProgress = startProgress + progressDelta;
      // Wrap progress between 0 and 1 so it loops infinitely
      newProgress = ((newProgress % 1) + 1) % 1;
      tween.progress(newProgress);
    },
    onThrowUpdate() {
      const progressDelta = this.x / distanceToTranslate;
      let newProgress = startProgress + progressDelta;
      newProgress = ((newProgress % 1) + 1) % 1;
      tween.progress(newProgress);
    },
    onRelease() {
      marquee.style.cursor = "grab";
    },
    onThrowComplete() {
      // Resume autoplay (respect hover state)
      const isHovering = marquee.matches(":hover");
      gsap.to(tween, { timeScale: isHovering ? 0 : 1, duration: 0.3 });
    },
  });

  // ---- DEBOUNCED RESIZE ----
  function debounce(func) {
    var timer;
    return function (event) {
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => func(), 500, event);
    };
  }
  window.addEventListener("resize", debounce(playMarquee));
};

document.addEventListener("DOMContentLoaded", init);
</script>
Step-7 Team section mouse event right

Then, in the team section, you need to insert the following script code to enable the mouse effect on the horizontal team wrapper.

<script>
  gsap.registerPlugin(Draggable, InertiaPlugin);

  const initBot = () => {
    const marquee = document.querySelector('[wb-data="marquee-bottom"]');
    if (!marquee) return;

    const duration = parseInt(marquee.getAttribute("duration"), 20) || 5;
    const marqueeContent = marquee.firstChild;
    if (!marqueeContent) return;

    // Clone for seamless loop
    const marqueeContentClone = marqueeContent.cloneNode(true);
    marquee.append(marqueeContentClone);

    // Make sure marquee can be grabbed
    marquee.style.cursor = "grab";

    let tween;
    let distanceToTranslate;

    const playMarquee = () => {
      let progress = tween ? tween.progress() : 0;
      tween && tween.progress(0).kill();

      const width = parseInt(
        getComputedStyle(marqueeContent).getPropertyValue("width"),
        10
      );
      const gap = parseInt(
        getComputedStyle(marqueeContent).getPropertyValue("column-gap"),
        10
      );
      distanceToTranslate = width + gap; // Reverse direction for bottom marquee (moving right)

      tween = gsap.fromTo(
        marquee.children,
        { x: -distanceToTranslate },
        { x: 0, duration, ease: "none", repeat: -1 }
      );
      tween.progress(progress);
    };

    playMarquee();

    // ---- PAUSE ON HOVER ----
    marquee.addEventListener("mouseenter", () => {
      if (tween) gsap.to(tween, { timeScale: 0, duration: 0.3 });
    });
    marquee.addEventListener("mouseleave", () => {
      if (tween) gsap.to(tween, { timeScale: 1, duration: 0.3 });
    });

    // ---- DRAGGABLE ----
    const proxy = document.createElement("div");
    let startProgress = 0;

    Draggable.create(proxy, {
      type: "x",
      trigger: marquee,
      inertia: true,
      onPressInit() {
        gsap.killTweensOf(tween);
        tween.timeScale(0);
        marquee.style.cursor = "grabbing";
        startProgress = tween.progress();
        gsap.set(proxy, { x: -distanceToTranslate });
      },
      onDrag() {
        // Adjusting for reverse direction (from left to right)
        // ProgressDelta should be calculated with respect to the direction of drag
        const progressDelta = this.x / distanceToTranslate;  // Directly using 'this.x' for forward movement
        let newProgress = startProgress + progressDelta;

        // Make sure progress is wrapped between 0 and 1 so that it loops infinitely
        newProgress = ((newProgress % 1) + 1) % 1;
        tween.progress(newProgress);
      },
      onThrowUpdate() {
        const progressDelta = this.x / distanceToTranslate; // Adjusted for rightward movement
        let newProgress = startProgress + progressDelta;
        newProgress = ((newProgress % 1) + 1) % 1;
        tween.progress(newProgress);
      },
      onRelease() {
        marquee.style.cursor = "grab";
      },
      onThrowComplete() {
        // Resume autoplay (respect hover state)
        const isHovering = marquee.matches(":hover");
        gsap.to(tween, { timeScale: isHovering ? 0 : 1, duration: 0.3 });
      },
    });

    // ---- DEBOUNCED RESIZE ----
    function debounce(func) {
      var timer;
      return function (event) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => func(), 500, event);
      };
    }
    window.addEventListener("resize", debounce(playMarquee));
  };

  document.addEventListener("DOMContentLoaded", initBot);
</script>
Grid preview with orange and gray colorGrid preview with orange and gray color
AI-powered systems for modern workflows
We are building the future powered by Cyntra
We are
Professional Man Portrait
Professional Man Portrait
Professional Woman Portrait
building
the future powered by Cyntra
Abstract white shape resembling an open circle merging into a right-pointing triangle on a black background.