Find easy to follow instructions
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.
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>
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);
}
}
);
});
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
}
});
});
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");
}
});
});
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
});
});
});
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,
}
});
});
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.
//===================================
// 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
}, "<");
});