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 - GSAP package
<!-- --------- Libraries --------- -->
<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://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/Draggable.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/dist/InertiaPlugin.min.js"></script>
Step 2 - Hero section animations
/* ============================
   MOUSE TRAIL - GSAP (Optimized)
   ============================ */
   document.addEventListener("DOMContentLoaded", () => {
  if (window.matchMedia("(min-width: 768px)").matches) {
    const images = document.querySelectorAll(".trail-img");
    const buttons = document.querySelectorAll(".content-button"); 
    let mouse = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
    let pos = { x: mouse.x, y: mouse.y };
    let isVisible = false;
    let hideTimeout;
    let isOverButton = false;

    // Start positions image
    gsap.set(images, { 
      x: mouse.x, 
      y: mouse.y, 
      opacity: 0, 
      scale: 0.7, 
      pointerEvents: "none", // important!
      rotate: () => gsap.utils.random(-10, 10)
    });

    // When mouse move
    window.addEventListener("mousemove", (e) => {
      if (isOverButton) return; // Stop animation when mouse on area 

      mouse.x = e.clientX;
      mouse.y = e.clientY;

      if (!isVisible) {
        gsap.to(images, { 
          opacity: 1, 
          scale: 1, 
          duration: 0.4, 
          stagger: 0.1, 
          ease: "power2.out" 
        });
        isVisible = true;
      }

      clearTimeout(hideTimeout);
      hideTimeout = setTimeout(() => {
        if (!isOverButton) {
          gsap.to(images, { opacity: 0, scale: 0.6, duration: 0.8 });
          isVisible = false;
        }
      }, 600);
    });

    // Loop animations
    function animateTrail() {
      pos.x += (mouse.x - pos.x) * 0.12;
      pos.y += (mouse.y - pos.y) * 0.12;

      if (isVisible && !isOverButton) {
        gsap.to(images, {
          x: pos.x,
          y: pos.y,
          duration: 0.5,
          ease: "power3.out",
          stagger: { each: 0.12, from: "end" }
        });
      }

      requestAnimationFrame(animateTrail);
    }
    animateTrail();

    // ===== BUTTON AREA =====
    buttons.forEach((btn) => {
      btn.addEventListener("mouseenter", () => {
        isOverButton = true;
        gsap.to(images, { 
          opacity: 0, 
          scale: 0, 
          duration: 0.3, 
          ease: "power1.inOut" 
        });
      });

      btn.addEventListener("mouseleave", () => {
        isOverButton = false;
        gsap.to(images, { 
          opacity: 1, 
          scale: 1, 
          duration: 0.4, 
          ease: "power1.out" 
        });
      });
    });
  }
});

/* =====================================================
     HERO SECTION
  ===================================================== */
gsap.registerPlugin(ScrollTrigger);
window.addEventListener("load", () => {
  
  const tl = gsap.timeline({ defaults: { ease: "power3.out" } });

  tl.from(".letter", {
    opacity: 0,
    y: 100,
    stagger: 0.12,
    duration: 1.2
  });
  tl.from(".hidden-image", {
    opacity: 0,
    y: 60,
    rotationY: 25,
    rotationZ: -5,
    scale: 0.9,
    transformOrigin: "center center",
    stagger: 0.15,
    duration: 1.6,
    ease: "power2.out"
  }, "-=1");
  tl.to(".hidden-image", {
    y: "+=20",
    rotationZ: "+=3",
    duration: 2.8,
    ease: "sine.inOut",
    yoyo: true,
    repeat: -1
  }, "-=0.5");

  tl.to(".letter", {
    y: 0,
    scale: 1.02,
    duration: 1.4,
    ease: "power1.inOut"
  }, "-=2");
  
  // === SPLIT TEXT ANIMATION ===
  const tagline = document.querySelector(".tag-on-hero");
  const paragraph = document.querySelector(".paragraph-hero");

  if (tagline) {
    const taglineSplit = new SplitText(tagline, { type: "chars" });
    gsap.fromTo(taglineSplit.chars,
      {
        opacity: 0,
        filter: "blur(15px)",
      },
      {
        opacity: 1,
        filter: "blur(0px)",
        duration: 0.8,
        ease: "power3.out",
        stagger: {
          each: 0.015,
          from: "random"
        }
      }
    );
  }

  if (paragraph) {
    const paragraphSplit = new SplitText(paragraph, { type: "lines" });
    gsap.fromTo(paragraphSplit.lines,
      {
        opacity: 0,
        y:40,
        filter: "blur(15px)",
      },
      {
        opacity: 1,
        y: 0,
        filter: "blur(0px)",
        duration: 1,
        ease: "power3.out",
        stagger: 0.15,
        scrollTrigger: {
          trigger: paragraph,
          start: "top 80%",
          toggleActions: "play none none none",
        },
      }
    );
  }

  // === BUTTONS FADE-IN ===
gsap.utils.toArray(".icon-wrapp-hero, .button-glow").forEach(el => {
  gsap.from(el, {
    opacity: 0,
    y: 20,
    duration: 1,
    ease: "back.out(2)",
    scrollTrigger: {
      trigger: el,
      start: "top 85%",
      toggleActions: "play none none none",
      markers: false
    }
  });
});

});
Step 3 - About us section animations
/* =====================================================
     ABOUT SECTION
  ===================================================== */
  gsap.registerPlugin(ScrollTrigger, SplitText);

window.addEventListener("load", () => {
  // === SPLIT TEXT ANIMATION ===
const taglineAbout = document.querySelector(".tag-on-about");

if (taglineAbout) {
  const taglineAboutSplit = new SplitText(taglineAbout, { type: "chars" });

  gsap.fromTo(
    taglineAboutSplit.chars,
    {
      opacity: 0,
      filter: "blur(15px)",
    },
    {
      opacity: 1,
      filter: "blur(0px)",
      duration: 1.1,
      ease: "power3.out",
      stagger: {
        each: 0.015,
        from: "random",
      },
      scrollTrigger: {
        trigger: taglineAbout,
        start: "top 80%", 
        toggleActions: "play none none none",
      },
    }
  );
}
  
  const aboutTexts = gsap.utils.toArray(".text-about");

  aboutTexts.forEach((text) => {
    const split = new SplitText(text, { type: "lines" });

    gsap.from(split.lines, {
      y: 90,
      opacity: 0,
      filter: "blur(20px)",
      duration: 1.3,
      ease: "power3.out",
      stagger: 0.15,
      scrollTrigger: {
        trigger: text,
        start: "top 85%",
        toggleActions: "play none none reverse",
      },
      onComplete: () => split.revert(),
    });
  });

  const sublineTexts = gsap.utils.toArray(".text-subline-about");

  sublineTexts.forEach((sub) => {
    const subSplit = new SplitText(sub, { type: "lines" });

    gsap.from(subSplit.lines, {
      y: 90,
      opacity: 0,
      filter: "blur(20px)",
      duration: 1.3,
      ease: "power3.out",
      stagger: 0.15,
      scrollTrigger: {
        trigger: sub,
        start: "top 80%",
        toggleActions: "play none none reverse",
      },
      onComplete: () => subSplit.revert(),
    });
  });
});
Step 4 - Service section animations
/* =====================================================
     SERVICE SECTION
  ===================================================== */
	document.addEventListener("DOMContentLoaded", () => {
  gsap.registerPlugin(ScrollTrigger);
	
  // === SPLIT TEXT ANIMATION ===
const taglineService = document.querySelector(".tag-on-service");

if (taglineService) {
  const taglineServiceSplit = new SplitText(taglineService, { type: "chars" });

  gsap.fromTo(
    taglineServiceSplit.chars,
    {
      opacity: 0,
      filter: "blur(15px)",
    },
    {
      opacity: 1,
      filter: "blur(0px)",
      duration: 1.1,
      ease: "power3.out",
      stagger: {
        each: 0.015,
        from: "random",
      },
      scrollTrigger: {
        trigger: taglineService,
        start: "top 80%", 
        toggleActions: "play none none none",
      },
    }
  );
}

  const text = document.querySelector(".text-heading-service");

  if (text) {
    const split = new SplitText(text, { type: "words" });

    gsap.set(split.words, {
      color: "#777",  
      opacity: 0.4,
      filter: "blur(12px)"
    });

    gsap.to(split.words, {
      color: "#000",  
      opacity: 1,
      filter: "blur(0px)",
      duration: 1.5,
      ease: "power2.out",
      stagger: 0.08,
      scrollTrigger: {
        trigger: text,
        start: "top 80%",
        end: "bottom 40%",
        scrub: true,
      }
    });
  }
});
Step 5 - Our team section animations
//=================================
// TEAM SECTION MULTI-MODE
//=================================

window.addEventListener("load", () => {
  gsap.registerPlugin(ScrollTrigger, Draggable);
	// === SPLIT TEXT ANIMATION ===
const taglineTeam = document.querySelector(".tag-on-team");

if (taglineTeam) {
  const taglineTeamSplit = new SplitText(taglineTeam, { type: "chars" });

  gsap.fromTo(
    taglineTeamSplit.chars,
    {
      opacity: 0,
      filter: "blur(15px)",
    },
    {
      opacity: 1,
      filter: "blur(0px)",
      duration: 1.1,
      ease: "power3.out",
      stagger: {
        each: 0.015,
        from: "random",
      },
      scrollTrigger: {
        trigger: taglineTeam,
        start: "top 80%", 
        toggleActions: "play none none none",
      },
    }
  );
}
  //=============================
  // SplitText Heading Animation
  //=============================
  function splitText(el) {
    const text = el.innerText;
    el.innerHTML = "";
    return [...text].map(char => {
      const span = document.createElement("span");
      span.textContent = char;
      if (char !== " ") span.style.display = "inline-block";
      el.appendChild(span);
      return span;
    });
  }

  const headings = document.querySelectorAll(".text-heading-team, .heading-team-two");
  let textTimeline;

  if (headings.length > 0) {
    textTimeline = gsap.timeline({
      scrollTrigger: {
        trigger: headings[0],
        start: "top 80%",
        toggleActions: "play none none none"
      }
    });

    headings.forEach((heading, i) => {
      const chars = splitText(heading);
      textTimeline.from(chars, {
        opacity: 0,
        filter: "blur(6px)",
        y: 40,
        stagger: 0.05,
        duration: 0.8,
        ease: "power3.out"
      }, i === 0 ? 0 : "<");
    });
  }
  
//=============================
// Card Animation + Draggable
//=============================
 if (window.innerWidth > 479) { 
    const cards = document.querySelectorAll(".card");
    const bounds = ".wrapper-team";  
    const container = document.querySelector(".cards");
    if (!cards.length || !container) return;

   // Tentukan posisi berdasarkan lebar layar
  let positions;

  if (window.innerWidth <= 766) {
    // MOBILE LANDSCAPE (480–766px)
    positions = [
      { x: -60, y: 140 },
      { x: 10, y: -10 },
      { x: 40, y: 80 },
      { x: 140, y: 30 },
      { x: -30, y: 120 },
      { x: 120, y: 140 }
    ];
  } else {
    // TABLET & DESKTOP (>766px)
    positions = [
      { x: -200, y: 60 },
      { x: 20, y: -20 },
      { x: 60, y: 100 },
      { x: 250, y: 40 },
      { x: -40, y: 180 },
      { x: 180, y: 200 }
    ];
  }
    cards.forEach((card, i) => {
      gsap.set(card, {
        top: "50%",
        left: "50%",
        xPercent: -50,
        yPercent: -50,
        scale: 1.5,
        opacity: 0,
        rotateY: 90
      });

      textTimeline.to(card, {
        x: positions[i].x,
        y: positions[i].y,
        scale: 1,
        opacity: 1,
        rotateY: 0,
        duration: 1.2,
        ease: "back.out(2)",
        delay: i * 0.1
      }, "<");

        // Label hover
      const label = card.querySelector(".card");
      if (label) {
        card.addEventListener("mouseenter", () => {
          gsap.to(label, { opacity: 1, y: -10, duration: 0.3, ease: "power2.out" });
        });
        card.addEventListener("mouseleave", () => {
          gsap.to(label, { opacity: 0, y: 0, duration: 0.3, ease: "power2.in" });
        });
      }

      // Draggable only for tablet & desktop
      Draggable.create(card, {
        type: "x,y",
        bounds:bounds,
        inertia: true,
        throwResistance: 1000,
        minDuration: 0.8,
        maxDuration: 2.2,
      });
    });


    container.addEventListener("mousemove", (e) => {
      const mouseX = e.clientX;
      const mouseY = e.clientY;

      cards.forEach(card => {
        const rect = card.getBoundingClientRect();
        const cardX = rect.left + rect.width / 2;
        const cardY = rect.top + rect.height / 2;

        const dx = mouseX - cardX;
        const dy = mouseY - cardY;
        const distance = Math.sqrt(dx * dx + dy * dy);

        const maxDist = 300;
        const influence = Math.max(0, 1 - distance / maxDist);

        gsap.to(card, {
          rotateY: dx * 0.03 * influence,
          duration: 0.4,
          ease: "power2.out"
        });
      });
    });

    container.addEventListener("mouseleave", () => {
      gsap.to(cards, { rotateY: 0, duration: 0.6, ease: "power2.out" });
    });
  }
});

  

//=================================
// PARAGRAPH ANIMATION 
//=================================
document.addEventListener("DOMContentLoaded", () => {
  if (typeof gsap === "undefined") {
    console.error("GSAP is not loaded");
    return;
  }

  const hasSplit = typeof SplitText !== "undefined";
  const hasScroll = typeof ScrollTrigger !== "undefined";

  if (hasScroll) gsap.registerPlugin(ScrollTrigger);
  if (hasSplit) gsap.registerPlugin(SplitText);

  const paragraphs = gsap.utils.toArray(".paragraph-team");

  paragraphs.forEach((par) => {
    if (hasSplit) {
      const paragSplit = new SplitText(par, { type: "chars" });
      const chars = paragSplit.chars;

      gsap.set(chars, { transformOrigin: "center bottom", y: 0.1 });

      const animOptions = {
        opacity: 0,
        y: 20,
        filter: "blur(12px)",
        duration: 0.9,
        ease: "power3.out",
        stagger: { each: 0.02, from: "random" }
      };

      if (hasScroll) {
        animOptions.scrollTrigger = {
          trigger: par,
          start: "top 85%",
          toggleActions: "play none none none"
        };
      }

      gsap.from(chars, animOptions);
    } else {
      gsap.from(par, {
        opacity: 0,
        y: 12,
        filter: "blur(10px)",
        duration: 0.8,
        ease: "power3.out",
        delay: 0.05
      });
      console.warn("SplitText not found — fallback animation used for .paragraph-team");
    }
  });
});
Step 6 - CTA section animations
/* =====================================================
     CTA SECTION
===================================================== */
gsap.registerPlugin(ScrollTrigger);

window.addEventListener("load", () => {

  // === ANIMASI TEXT CONTACT ===
  const contactTexts = [".text-contact-first", ".text-contact-second"];

  contactTexts.forEach((cls) => {
    const el = document.querySelector(cls);
    if (el) {
      const split = new SplitText(el, { type: "chars" });

      gsap.from(split.chars, {
        opacity: 0,
        y: 80,
        filter: "blur(10px)",
        duration: 1.2,
        ease: "power3.out",
        stagger: 0.05,
        scrollTrigger: {
          trigger: el,
          start: "top 70%",
          toggleActions: "play none none none",
          markers: false
        }
      });
    }
  });

  // === ANIMASI SUBLINE CONTACT ===
  const subline = document.querySelector(".text-subline-contact");
  if (subline) {
    const sublineSplit = new SplitText(subline, { type: "lines" });

    gsap.from(sublineSplit.lines, {
      opacity: 0,
      filter: "blur(15px)",
      duration: 1.2,
      ease: "power3.out",
      stagger: 0.15,
      scrollTrigger: {
        trigger: subline,
        start: "top 85%",
        toggleActions: "play none none reverse",
        markers: false
      }
    });
  }

  // === ANIMASI BUTTON CTA ===
  gsap.utils.toArray(".button-glow-cta").forEach((el) => {
    gsap.from(el, {
      opacity: 0,
      y: 20,
      duration: 1,
      ease: "back.out(2)",
      scrollTrigger: {
        trigger: el,
        start: "top 85%",
        toggleActions: "play none none none",
        markers: false
      }
    });
  });
});
Step 7 - Footer menu animations
// ============================
// FOOTER ANIMATION
// ============================
document.addEventListener("DOMContentLoaded", () => {
  gsap.registerPlugin(ScrollTrigger);

  // Timeline Footer
  const footerTl = gsap.timeline({
    scrollTrigger: {
      trigger: "footer", 
      start: "top 70%",
      toggleActions: "play none none none",
    },
    defaults: { ease: "power3.out", duration: 1 }
  });

  // 1️⃣ Inner logo wrapper
  footerTl.from(".inner-wrapper-logo", {
    opacity: 0,
    filter: "blur(30px)",
    duration: 2,
  });

  // 2️⃣ Link-page (4 link text)
  footerTl.from(".link-page", {
    opacity: 0,
    y: 60,
    filter: "blur(20px)",
    stagger: 0.15,
  }, "-=1");

  // 3️⃣ Icon wrapper (3 icon, start from center)
  footerTl.from(".icon-wrapp-footer", {
    opacity: 0,
    y: 60,
    filter: "blur(20px)",
    stagger: {
      each: 0.12,
      from: "center"
    },
  }, "-=0.9");

  // 4️⃣ Content link website text
  footerTl.from(".content-link-website", {
    opacity: 0,
    y: 40,
    filter: "blur(20px)",
  }, "-=0.7");
});
Step 8 - Navbar section animations
/* =====================================================
     NAVBAR HOVER ANIMATION
  ===================================================== */
  document.addEventListener("DOMContentLoaded", () => {
  if (typeof gsap === "undefined") {
    console.error("GSAP not found — include GSAP before this script.");
    return;
  }

  // helper: split element text into spans (chars) and return NodeList of chars
  function splitChars(el) {
    const text = (el.textContent || "").trim();
    el.innerHTML = ""; // remove current text
    const frag = document.createDocumentFragment();
    [...text].forEach(ch => {
      const span = document.createElement("span");
      span.className = "nav-char";
      span.textContent = ch === " " ? "\u00A0" : ch;
      // ensure transform effects are visible
      span.style.display = "inline-block";
      frag.appendChild(span);
    });
    el.appendChild(frag);
    return el.querySelectorAll("span.nav-char");
  }

  document.querySelectorAll('.navbar-item').forEach(item => {
    // try common structures: .default and .hidden inside .navbar-item
    const defaultEl = item.querySelector('.default');
    const hiddenEl  = item.querySelector('.hidden');

    // if neither exist, try a single .navbar-item-text fallback (no alt text)
    if (!defaultEl && !hiddenEl) {
      console.warn(".navbar-item has no .default/.hidden children:", item);
      return;
    }

    // ensure parents clip their children so yPercent motion is visible
    [defaultEl, hiddenEl].forEach(el => {
      if (!el) return;
      el.style.overflow = "hidden";
      el.style.display = "inline-block";
      el.style.verticalAlign = "middle";
    });

    const defaultChars = defaultEl ? splitChars(defaultEl) : [];
    const hiddenChars  = hiddenEl  ? splitChars(hiddenEl)  : [];

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

    // mouse handlers
    item.addEventListener('mouseenter', () => {
      gsap.killTweensOf([defaultChars, hiddenChars]);
      gsap.to(defaultChars, {
        yPercent: -100,
        opacity: 0,
        stagger: 0.04,
        duration: 0.38,
        ease: "power2.out"
      });
      gsap.to(hiddenChars, {
        yPercent: 0,
        opacity: 1,
        stagger: 0.04,
        duration: 0.38,
        ease: "power2.out"
      });
    });

    item.addEventListener('mouseleave', () => {
      gsap.killTweensOf([defaultChars, hiddenChars]);
      gsap.to(defaultChars, {
        yPercent: 0,
        opacity: 1,
        stagger: 0.03,
        duration: 0.36,
        ease: "power2.in"
      });
      gsap.to(hiddenChars, {
        yPercent: 100,
        opacity: 0,
        stagger: 0.03,
        duration: 0.36,
        ease: "power2.in"
      });
    });
  });
});
Note for service sections

If you want to change or add a new image to the cards in the service section, you need to open the part shown in the image below. You can change image on class "image-service".

A part for page instructions