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 - Library and package GSAP

Before that, prepare the library package from GSAP for some animations. This animation makes the scrolling experience smoother across all devices.

<!-- --------- Libraries --------- -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/SplitText.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/TextPlugin.min.js"></script>
<script src="https://unpkg.com/lenis@1.3.1/dist/lenis.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/Draggable.min.js"></script>
Step 2 - Hero section animations

Animations for headline, subline, and mouse trail.

 //===================================
  // HERO SECTION
  //===================================
  
  document.addEventListener("DOMContentLoaded", () => {
  const cards = gsap.utils.toArray(".card-hero");
  const mm = gsap.matchMedia();

  mm.add(
    {
      isDesktop: "(min-width: 992px)",
      isTablet: "(min-width: 768px) and (max-width: 991px)",
      isMobile: "(max-width: 767px)"
    },
    (context) => {
      let { isDesktop, isTablet, isMobile } = context.conditions;

      const gapDelay = 0.01;
      let stepX, stepY;

      if (isDesktop) {
        stepX = 70;
        stepY = 20;
      } else if (isTablet) {
        stepX = 40;
        stepY = 15;
      } else if (isMobile) {
      	stepX = 30; 
      	stepY = 12;
      }

      // =========================
      // 🔹 HERO ANIMATION
      // =========================
      if (isDesktop || isTablet || isMobile) {
        let tl = gsap.timeline();

        tl.from(cards, {
          scale: 0.9,
          y: "350%",
          opacity: 0,
          duration: 0.8,
          ease: "power3.out",
          stagger: 0.15
        });
		
      tl.to(cards, {
        x: (i) => {
          if (isMobile) {
            return (i - (cards.length - 1) / 2) * stepX;
          } else {
            return i * stepX;
          }
        },
        y: (i) => i * stepY,
        duration: 0.8,
        ease: "sine.inOut",
        stagger: { each: 0.15, from: isDesktop ? "end" : "center" },
        onComplete: () => {
          cards.forEach((card, i) => {
            if (isMobile) {
              let offset = i - (cards.length - 1) / 2;
              card.dataset.baseX = offset * stepX;
              card.dataset.baseY = i * stepY;
            } else {
              card.dataset.baseX = i * stepX;
              card.dataset.baseY = i * stepY;
            }
          });
        }
      }, `+=${gapDelay}`);
        
       
        tl.add(() => {
          gsap.to(cards, {
            yPercent: 5,
            duration: 2,
            ease: "sine.inOut",
            repeat: -1,
            yoyo: true,
            stagger: 0.2
          });
        });
      }

      // =========================
      // 🔹 DRAGGABLE HERO
      // =========================
      
  
        Draggable.create(".card-hero", {
          type: "x,y",
          edgeResistance: 1,
          inertia: true,
          inertiaResistance: 80,                
  		  inertiaDuration: { min: 1, max: 1.5 },     
 		  inertiaEase: "power2.out",              
          bounds: window,
          onPress() {
            if (window.showCursor) window.showCursor("grab");
          },
          onRelease() {
            if (window.showCursor) window.showCursor("hover");
          },
          onDrag() {
            const e = this.pointerEvent || this.event;
            if (e && window.updateCursorPosition) {
              window.updateCursorPosition(e.clientX, e.clientY);
            }
          },
          onDragStart: function () {
            const baseX = parseFloat(this.target.dataset.baseX) || 0;
            const baseY = parseFloat(this.target.dataset.baseY) || 0;
            gsap.set(this.target, { x: baseX + this.x, y: baseY + this.y });
          }
        });
      
    }
  );
  
    
    let split = new SplitText(".heading-text-hero", { type: "lines" });

 	gsap.from(split.lines, {
    yPercent:100,
    opacity: 0,
    duration: 1,
    ease: "power4.out",
    stagger: 0.06,
    delay: 0.4
  });
  let splitDesc = new SplitText(".inner-paragraph", { type: "lines" });

 		gsap.from(splitDesc.lines, {
    yPercent: 100,
    opacity: 0,
    duration: 2,
    ease: "power4.out",
    stagger: 0.1,
    delay: 0.4
  });
  	
  
  // MOUSE TRAIL
  const section = document.querySelector(".wrapper-hero-outer"); 

  section.style.position = "relative";
  section.style.overflow = "hidden";

  section.addEventListener("mousemove", (e) => {
    for (let i = 0; i < 2; i++) { 
      const particle = document.createElement("span");
      particle.style.position = "absolute";
      particle.style.opacity = 0.8;
      particle.style.pointerEvents = "none";
      particle.style.zIndex = 0; 

      // random shape
      const shapeType = Math.floor(Math.random() * 3);
      if (shapeType === 0) { // circle
        particle.style.width = "4px";
        particle.style.height = "4px";
        particle.style.borderRadius = "50%";
        particle.style.background = "#fff";
      } else if (shapeType === 1) { // square
        particle.style.width = "4px";
        particle.style.height = "4px";
        particle.style.background = "#fff";
      } else { // triangle
        particle.style.width = 0;
        particle.style.height = 0;
        particle.style.borderLeft = "5px solid transparent";
        particle.style.borderRight = "5px solid transparent";
        particle.style.borderBottom = "8px solid #fff";
      }

      const rect = section.getBoundingClientRect();
      particle.style.left = e.clientX - rect.left + "px";
      particle.style.top = e.clientY - rect.top + "px";

      section.appendChild(particle);

      gsap.to(particle, {
        x: (Math.random() - 0.5) * 80, 
        y: (Math.random() - 0.5) * 80, 
        scale: 0.5 + Math.random() * 1.2,
        opacity: 0,
        duration: 0.8 + Math.random() * 0.5,
        ease: "power2.out",
        onComplete: () => particle.remove()
      });
    }
  });
});
	// END MOUSE TRAIL

  
  const counters = document.querySelectorAll(".count");

  counters.forEach(counter => {
    let target = +counter.getAttribute("data-target");

    gsap.fromTo(counter, 
      { innerText: 0 }, 
      {
        innerText: target,
        duration: 2,
        snap: { innerText: 1 }, 
        ease: "power1.out",
        onUpdate: function () {
          counter.textContent = Math.floor(counter.innerText);
        }
      }
    );
  });
Step 3 - About us section animations

Entrance about us section.

 //===================================
  // ABOUT SECTION
  //===================================
  
 document.addEventListener("DOMContentLoaded", function () {
  // if (!allowAnimation()) return;
 
  if (typeof gsap === "undefined") {
    console.error("Undifined GSAP");
    return;
  }
  gsap.registerPlugin(ScrollTrigger);

  let counters = gsap.utils.toArray(".count");
  if (!counters.length) counters = gsap.utils.toArray("[data-target]");

  if (!counters.length) {
    console.warn("Null! [data-target].");
    return;
  }

  const section =
    document.querySelector(".section-achievement") || 
    counters[0].closest("section") ||                
    counters[0].parentElement;                       

  let started = false;

  ScrollTrigger.create({
    trigger: ".wrapper-about",
    start: "top 60%",   
    once: true,
   	markers: false,   
    onEnter: () => {
      if (started) return;
      started = true;

      counters.forEach((el, idx) => {
        const raw = el.getAttribute("data-target") || el.dataset.target || el.textContent;
        const target = parseInt(String(raw).replace(/\D/g, ""), 10) || 0;
        const duration = Math.max(1, Math.min(3, (target / 200) * 2));
        const obj = { val: 0 };

        gsap.to(obj, {
          val: target,
          duration: duration,
          ease: "none", 
          delay: idx * 0.15, 
          onUpdate: () => {
            
            el.textContent = Math.floor(obj.val).toLocaleString();
          },
          onComplete: () => {
            el.textContent = target.toLocaleString();
          }
        });
      });
    }
  });
});
	
   gsap.registerPlugin(ScrollTrigger, SplitText);

  document.addEventListener("DOMContentLoaded", function () {
    const targets = [".text-about", ".paragraph-description-about"];

    targets.forEach((selector) => {
      const el = document.querySelector(selector);
      if (!el) return;

      const split = new SplitText(el, { 
        type: "lines",
        linesClass: "lineChild",
        splitClass: "split"
      });

      console.log("Split lines for", selector, split.lines);

      gsap.from(split.lines, {
        yPercent: 100,
        opacity: 0,
        duration: 2,
        ease: "power4.out",
        stagger: 0.08,
        scrollTrigger: {
          trigger: selector,
          start: "top 65%",
          once: true
        }
      });
    });

    gsap.from(".line-break-about", {
      x: "-200%",           
      duration: 1.5,
      ease: "power3.out",
      scrollTrigger: {
        trigger: ".wrapper-about",
        start: "top 30%",
        once: true
      }
    });
  });
Step 4 - Service section animations

Entrance section with splitText.

//===================================
  // SERVICE SECTION
  //===================================
  document.addEventListener("DOMContentLoaded", function () {
  //if (!allowAnimation()) return;
  
  if (typeof gsap === "undefined") {
    console.error("GSAP Error!.");
    return;
  }
  gsap.registerPlugin(ScrollTrigger);

  const wrapper = document.querySelector(".wrapper-service");
  const mainTL = gsap.timeline({
    scrollTrigger: {
      trigger: wrapper || document.body,
      start: "top 70%",
      toggleActions: "play none none none",
      // markers: true
    }
  });

  // === Heading + Paragraph ===
  const headEl = document.querySelector(".head-service");
  const paraEl = document.querySelector(".paragraph-service");

  function safeSplitLines(el) {
    if (!el) return null;
    if (typeof SplitText === "undefined") return null;
    try {
      return new SplitText(el, { type: "lines", linesClass: "split-line" });
    } catch {
      return null;
    }
  }

  const headSplit = safeSplitLines(headEl);
  const paraSplit = safeSplitLines(paraEl);

  if (headSplit) {
    mainTL.from(headSplit.lines, {
      yPercent: 100,
      opacity: 0,
      duration: 2,
      ease: "power4.out",
      stagger: 0.06
    });
  }
  if (paraSplit) {
    mainTL.from(paraSplit.lines, {
      yPercent: 100,
      opacity: 0,
      duration: 2,
      ease: "power4.out",
      stagger: 0.08
    }, "<");
  }

  // === Service Cards ===
  
  const cards = gsap.utils.toArray(".card-text"); 
  cards.forEach((card, idx) => { 
  const number = card.querySelector(".number-service"); 
  const title = card.querySelector(".item-card"); 
  const paragraph = card.querySelector(".paragraph-card"); 
  
  let splitTitle = null; 
  	if (title && typeof SplitText !== "undefined") { 
    	try { 
      splitTitle = new SplitText(title, { 
      type: "chars", 
      charsClass: "split-char" 
      }); 
      } catch (e) { 
      console.warn("SplitText chars error", title, e); 
      } 
   	} const cardDelay = idx * 0;

    // === number-service ===
   if (number) {
      const n1 = gsap.from(number, {
        opacity: 0,
        y: 60,
        filter: "blur(10px)",
        duration: 0.7,
        ease: "back.out(1.6)",
      });
      mainTL.add(n1, "+=" + cardDelay);
    }

    // === item-card ===
    if (splitTitle && splitTitle.chars.length) {
      const t1 = gsap.from(splitTitle.chars, {
        opacity: 0,
        scale: 0,
        filter: "blur(10px)",
        duration: 0.8,
        ease: "back.out(1.6)",
        stagger: 0.015
      });
      mainTL.add(t1, "-=0.9"); 
    } else if (title) {
      const t1 = gsap.from(title, {
        opacity: 0,
        y: 10,
        scale: 0.95,
        duration: 0.8,
        ease: "power2.out"
      });
      mainTL.add(t1, "-=0.9");
    }

    // === paragraph-card ===
    if (paragraph) {
      const t2 = gsap.from(paragraph, {
        y: 24,
        opacity: 0,
        duration: 0.7,
        ease: "power3.out",
        stagger: 0.02
      });
      mainTL.add(t2, "-=0.9");
    }
  });
});
Step 5 - Project section animations

Entrance headline with splitText effects.

//===================================
  // PROJECTS SECTION
  //===================================
	document.addEventListener("DOMContentLoaded", function () {
  
  if (typeof gsap === "undefined") {
    console.error("GSAP Error!.");
    return;
  }
  gsap.registerPlugin(ScrollTrigger);

  if (typeof SplitText === "undefined") {
    console.error("SplitText Error!.");
    return;
  }

  document.querySelectorAll(".heading-projects-one, .heading-projects-two").forEach(heading => {
    const split = new SplitText(heading, { type: "lines" });

    gsap.from(split.lines, {
      scrollTrigger: {
        trigger: heading,
        start: "top 70%",
        toggleActions: "play none none none",
      },
      yPercent: 100,
      opacity: 0,
      duration: 2,
      ease: "power4.out",
      stagger: 0.09
    });
  });
});

Step 6 - Team section animations

Entrance headline.


	//===================================
  // TEAM SECTION
  //===================================

document.addEventListener("DOMContentLoaded", () => {
  gsap.registerPlugin(ScrollTrigger);

  const text = document.querySelector(".text-reveal-team");
  const split = new SplitText(text, { type: "words" });

  gsap.set(split.words, { color: "#000" });

  gsap.to(split.words, {
    color: "#fff",
    ease: "none",
    stagger: 0.1,
    scrollTrigger: {
      trigger: text,
      start: "top 80%",
      end: "bottom 20%",
      scrub: true,   
    }
  });
});
Step 7 - Navbar menu animations

In the animation section of the navbar menu, you need to combine several style classes.

/* =====================================================
     NAVBAR HOVER ANIMATION
  ===================================================== */
  
document.addEventListener("DOMContentLoaded", () => {

  if (typeof gsap === "undefined") {
    console.error("GSAP belum dimuat");
    return;
  }

 function splitChars(el) {
    if (!el) return [];
    const text = el.textContent.trim();
    el.textContent = "";
    const chars = [];
    [...text].forEach(ch => {
      const span = document.createElement("span");
      span.textContent = ch === " " ? "\u00A0" : ch;
      span.style.display = "inline-block";
      el.appendChild(span);
      chars.push(span);
    });
    return chars;
  }


  document.querySelectorAll(".navbar-item").forEach(item => {
    const defaultEl = item.querySelector(".navbar-item-text.default");
    const hiddenEl = item.querySelector(".navbar-item-text.hidden");

    if (!defaultEl || !hiddenEl) return;

    const defaultChars = splitChars(defaultEl);
    const hiddenChars = splitChars(hiddenEl);

    gsap.set(defaultChars, { yPercent: 0, opacity: 1 });
    gsap.set(hiddenChars, { yPercent: 100, opacity: 0 });

    const tl = gsap.timeline({ paused: true, defaults: { duration: 0.4, ease: "power2.out" } });
    tl.to(defaultChars, {
      yPercent: -100,
      opacity: 0,
      stagger: 0.03
    }, 0)
    .to(hiddenChars, {
      yPercent: 0,
      opacity: 1,
      stagger: 0.03
    }, 0.05);

    // hover control
    item.addEventListener("mouseenter", () => tl.play());
    item.addEventListener("mouseleave", () => tl.reverse());
  });
});

In the animation section of the navbar menu, you need to combine several style classes.

Step 8 - Footer section animations

	//===================================
  // FOOTER SECTION
  //===================================
  document.addEventListener("DOMContentLoaded", function () {
  if (!allowAnimation()) return;
  if (typeof gsap === "undefined") {
    console.error("GSAP Error!.");
    return;
  }
  gsap.registerPlugin(ScrollTrigger);

  if (typeof SplitText === "undefined") {
    console.error("SplitText Error!.");
    return;
  }

  // === Timeline Footer ===
  const tlFooter = gsap.timeline({
    scrollTrigger: {
      trigger: ".wrapper-footer",
      start: "top 80%",     
      toggleActions: "play none none none",

    }
  });

  // === Line Break Footer ===
  tlFooter.from(".line-break, .logo-invert", {
    width: 0,
    duration: 1.2,
    ease: "power3.out"
  });

  // === Text Footer (SplitText) ===
  document.querySelectorAll(".text-footer").forEach(text => {
    const split = new SplitText(text, { type: "words" });

    tlFooter.from(split.words, {
      opacity: 0,
      scale: 0,
      filter: "blur(10px)",
      duration: 0.8,
      ease: "back.out(1.6)",
      stagger: 0.015
    }, "-=0.8"); 
  });

  // === Wrapper Social Media (icons) ===
  tlFooter.from(".wrapper-social-media .icon", {
    scale: 0,
    opacity: 0,
    duration: 0.8,
    ease: "back.out(1.7)",
    stagger: 0.15
  }, "-=1"); 

  // === Bot Inner Content (quick links) ===
  tlFooter.from(".bot-inner-content .quick-link", {
    scale: 0,
    opacity: 0,
    duration: 0.6,
    ease: "back.out(1.7)",
    stagger: 0.1
  }, "-=0.7");
  
   // === Bot ===
  tlFooter.from(".bottom-wrapper .link-bottom-footer", {
    scale: 0,
    opacity: 0,
    duration: 0.5,
    ease: "back.out(1.7)",
    stagger: 0.1
  }, "<");
});