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://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.3.1/dist/lenis.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/SplitText.min.js"></script>
<script>
window.Webflow ||= [];
window.Webflow.push(() => {
gsap.registerPlugin(ScrollTrigger, SplitText);
/* =====================================================
SMOOTH SCROLLING (Desktop & Tablet Only)
===================================================== */
if (window.matchMedia("(min-width: 768px)").matches) {
const lenis = new Lenis({
duration: 1.2,
easing: (t) => 1 - Math.pow(1 - t, 3),
smoothWheel: true,
smoothTouch: false
});
function raf(time) {
lenis.raf(time);
ScrollTrigger.update();
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
lenis.on("scroll", ScrollTrigger.update);
} else {
console.log("Lenis disabled on mobile");
}
Animations for headline, subheadline, and background.
/*======================================================
MOUSE PARALLAX
========================================================*/
document.addEventListener("mousemove", (e) => {
const sectionX = (e.clientX / window.innerWidth - 0.5) * 50;
const sectionY = (e.clientY / window.innerHeight - 0.5) * 30;
gsap.to(".image-parallax", {
x: sectionX,
y: sectionY,
transformPerspective: 800,
ease: "power2.out",
duration: 0.6
});
const objX = -(e.clientX / window.innerWidth - 0.5) * 40;
const objY = -(e.clientY / window.innerHeight - 0.5) * 40;
gsap.to(".image-hero", {
x: objX,
y: objY,
ease: "power2.out",
duration: 1
});
});
/* =====================================================
HERO SECTION
===================================================== */
const split = new SplitText(".paragraph-second-hero, .tag-tittle-hero", {type : "lines"});
gsap.from(split.lines, {
opacity: 0,
y: 40,
filter: "blur(15px)",
duration: 0.85,
ease: "power2.out",
stagger: 0.25
});
const paragraph = document.querySelector(".wrapper-paragraph-hero");
const crystal = document.querySelector(".wrapper-image-hero");
if (paragraph && crystal) {
if (window.matchMedia("(min-width: 768px)").matches) {
gsap.set(paragraph, { opacity: 0, visibility: "hidden" });
const split = new SplitText(paragraph, { type: "words,chars" });
const hoverTimeline = gsap.timeline({ paused: true })
.from(split.chars, {
opacity: 0,
scale: 0,
filter: "blur(20px)",
rotateX: 90,
stagger: { amount: 1, from: "random" },
ease: "back.out(1.7)",
duration: 0.4,
immediateRender: false
});
crystal.addEventListener("mouseenter", () => {
gsap.set(paragraph, { visibility: "visible" });
gsap.to(paragraph, { opacity: 1, scale:1, filter:"blur(0px)", duration: 0.3 });
hoverTimeline.play(0);
});
crystal.addEventListener("mouseleave", () => {
gsap.to(paragraph, {
opacity: 0,
duration: 0.3,
onComplete: () => gsap.set(paragraph, { visibility: "hidden" })
});
hoverTimeline.reverse();
});
} else {
gsap.set(paragraph, { opacity: 1, visibility: "visible" });
gsap.from(paragraph,{
opacity : 0,
y: 20,
filter: "blur(20px)",
duration: 0.35
})
}
}
//Hover Button
const btn = document.querySelector('.wrapper-btn-hero');
btn.addEventListener('mouseenter', () => {
gsap.to(btn, {
duration: 0.4,
boxShadow: "0 0 8px #ff3900, 0 0 12px #ff3900",
ease: "power2.out" });
});
btn.addEventListener('mouseleave', () => {
gsap.to(btn, {
duration: 0.4,
boxShadow: "0 0 0px transparent",
ease: "power2.in" });
});
Entrance about our section, with blur and skew animations.
/* =====================================================
ABOUT SECTION
===================================================== */
const about = document.querySelector(".wrapper-about");
if (about) {
about.style.perspective = "1000px";
about.style.transformStyle = "preserve-3d";
gsap.from([".text-about-first", ".text-about-second", ".text-about-third"], {
opacity: 0,
scaleY: 0,
scaleX: 1.2,
skewY: 5,
rotateX: 45,
filter: "blur(10px)",
transformOrigin: "center bottom",
duration: 1.2,
ease: "power3.out",
stagger: 0.25,
scrollTrigger: {
trigger: ".wrapper-about",
start: "top 40%",
end: "top 30%",
toggleActions: "play none none none" }
});
}
Images follow the cursor when moving the mouse.
/* =====================================================
MOUSE TRAIL
===================================================== */
if (window.matchMedia("(min-width: 768px)").matches) {
const images = document.querySelectorAll(".trail-img");
let mouse = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
let pos = { x: mouse.x, y: mouse.y };
let moveTimeout;
let isVisible = false;
gsap.set(images, { x: mouse.x, y: mouse.y, opacity: 0, scale: 0 });
let lastMouse = { x: mouse.x, y: mouse.y };
const threshold = 15;
window.addEventListener("mousemove", (e) => {
const dx = Math.abs(e.clientX - lastMouse.x);
const dy = Math.abs(e.clientY - lastMouse.y);
if (dx < threshold && dy < threshold) return;
mouse.x = e.clientX;
mouse.y = e.clientY;
lastMouse.x = mouse.x;
lastMouse.y = mouse.y;
if (!isVisible) {
gsap.to(images, { opacity: 1, scale: 1, duration: 0.5, stagger: 0.1 });
isVisible = true;
}
clearTimeout(moveTimeout);
moveTimeout = setTimeout(() => {
gsap.to(images, { opacity: 0, scale: 0, duration: 0.8 });
isVisible = false;
}, 400);
});
function animateTrail() {
pos.x += (mouse.x - pos.x) * 0.08;
pos.y += (mouse.y - pos.y) * 0.08;
if (isVisible) {
gsap.to(images, {
x: pos.x,
y: pos.y,
duration: 0.8,
ease: "power3.out",
stagger: {
each: 0.15,
from: "start"
}
});
}
requestAnimationFrame(animateTrail);
}
animateTrail();
}
Entrance headline, and hover effects.
/* =====================================================
SERVICE SECTION
===================================================== */
// TAGLINE ANIMATION
const taglineSplit = getSplit('.tagline-all', 'chars');
gsap.from(taglineSplit.chars, {
scrollTrigger: {
trigger: ".tagline-all",
start: "top 80%",
toggleActions: "play none none none"
},
scale: 0,
filter: "blur(20px)",
opacity: 0,
duration: 0.8,
stagger: { each: 0.05, from: "random" },
ease: "back.out(1.7)"
});
// DESCRIPTION
const lines = document.querySelectorAll(".text-service-description");
gsap.from(lines, {
scrollTrigger: {
trigger: ".wrapper-service",
start: "top 70%",
toggleActions: "play none none none",
markers: false
},
y: 40,
opacity: 0,
filter: "blur(8px)",
duration: 0.8,
ease: "power2.out",
stagger: 0.2
});
//Start hover
if (window.matchMedia("(min-width: 480px)").matches) {
document.querySelectorAll(".overflow-hide-image").forEach(overflow => {
const card = overflow.closest(".tag-jobs");
const tags = card?.querySelectorAll(".stroke-tag-job");
const img = card?.querySelector(".image-service");
gsap.set(tags, { y: -30, opacity: 0 });
if (img) {
gsap.set(img, { y: 30, opacity: 0, rotateZ: 0 });
}
const tl = gsap.timeline({ paused: true });
tl.to(tags, {
y: 0,
opacity: 1,
duration: 0.4,
stagger: 0.1,
ease: "power2.out"
});
if (img) {
tl.to(img, {
y: 0,
opacity: 1,
rotateZ: 15,
duration: 0.5,
ease: "power2.out"
}, "<");
}
overflow.addEventListener("mouseenter", () => tl.play());
overflow.addEventListener("mouseleave", () => tl.reverse());
});
}
Entrance headline, Card positions and hover effects.
/* =====================================================
TEAM SECTION
===================================================== */
const teamSplit = getSplit(".subline-team ", "lines");
gsap.from(teamSplit.lines, {
scrollTrigger: {
trigger: ".subline-team",
start: "top 80%",
toggleActions: "play none none none",
once: true
},
y: 40,
filter: "blur(20px)",
opacity: 0,
duration: 0.8,
ease: "power2.out"
});
gsap.from(".inner-text-team > *", {
scrollTrigger: {
trigger: ".wrapper-text-team",
start: "top 75%",
toggleActions: "play none none none",
markers: false
},
opacity: 0,
y: 40,
filter: "blur(8px)",
duration: 0.8,
ease: "power2.out",
stagger: 0.15
});
const teamTagSplit = getSplit('.tagline-team', 'chars');
gsap.from(teamTagSplit.chars, {
scrollTrigger: {
trigger: ".tagline-team",
start: "top 80%",
toggleActions: "play none none reverse"
},
scale: 0,
filter: "blur(20px)",
opacity: 0,
duration: 0.8,
stagger: { each: 0.05, from: "random" },
ease: "back.out(1.7)"
});
let mm = gsap.matchMedia();
mm.add("(min-width: 768px)", () => {
const positions = [
{ selector: ".pos-one", props: { x: 40, y: 40, z: -60, rotationY: -25, scale: 0.85, zIndex: 1, filter: "grayscale(100%)" } },
{ selector: ".pos-two", props: { y: 40, rotationY: -15, scale: 0.92, zIndex: 2, filter: "grayscale(100%)" } },
{ selector: ".pos-three", props: { y: 40, rotationY: 0, scale: 1, zIndex: 3, filter: "grayscale(100%)" } },
{ selector: ".pos-four", props: { y: 40, rotationY: 15, scale: 0.92, zIndex: 2, filter: "grayscale(100%)" } },
{ selector: ".pos-five", props: { x: -40, y: 40, z: -60, rotationY: 25, scale: 0.85, zIndex: 1, filter: "grayscale(100%)" } }
];
positions.forEach(pos => {
const el = document.querySelector(pos.selector);
if (el) {
gsap.set(el, pos.props);
el.dataset.original = JSON.stringify(pos.props);
}
});
document.querySelectorAll(".pos-one, .pos-two, .pos-three, .pos-four, .pos-five").forEach(card => {
card.addEventListener("mouseenter", () => {
if (window.innerWidth < 768) return;
let animProps = {
y: -10,
rotationY: 0,
scale: 1.05,
zIndex: 5,
filter: "grayscale(0%)",
duration: 0.4,
ease: "power2.out"
};
if (card.classList.contains("pos-one")) animProps.x = 20;
if (card.classList.contains("pos-five")) animProps.x = -20;
gsap.to(card, animProps);
});
card.addEventListener("mouseleave", () => {
if (window.innerWidth < 768) return;
const original = JSON.parse(card.dataset.original);
gsap.to(card, { ...original, duration: 0.4, ease: "power2.inOut" });
});
});
});
In the animation section of the navbar menu, you need to combine several style classes.
/* =====================================================
NAVBAR HOVER ANIMATION
===================================================== */
document.querySelectorAll('.navbar-item-text').forEach(el => {
const letters = el.textContent.trim().split('');
el.innerHTML = letters.map(l => {
if (l === " ") {
return `<span> </span>`;
} else {
return `<span>${l}</span>`;
}
}).join('');
});
document.querySelectorAll('.navbar-item').forEach(item => {
const defaultSpans = item.querySelectorAll('.default span');
const hiddenSpans = item.querySelectorAll('.hidden span');
gsap.set(hiddenSpans, { yPercent: 100, opacity: 0 });
item.addEventListener('mouseenter', () => {
gsap.to(defaultSpans, {
yPercent: -100,
opacity: 0,
stagger: 0.05,
duration: 0.4,
ease: "power2.out"
});
gsap.to(hiddenSpans, {
yPercent: 0,
opacity: 1,
stagger: 0.05,
duration: 0.4,
ease: "power2.out"
});
});
item.addEventListener('mouseleave', () => {
gsap.to(defaultSpans, {
yPercent: 0,
opacity: 1,
stagger: 0.05,
duration: 0.4,
ease: "power2.in"
});
gsap.to(hiddenSpans, {
yPercent: 100,
opacity: 0,
stagger: 0.05,
duration: 0.4,
ease: "power2.in"
});
});
});
In the animation section of the navbar menu, you need to combine several style classes.
/* =====================================================
FOOTER LINKS
===================================================== */
document.querySelectorAll(".text-link").forEach(link => {
const split = new SplitText(link, { type: "chars" });
link.split = split;
const spans = split.chars;
link.addEventListener("mouseenter", () => {
gsap.to(spans, { color: "#ff3900", stagger: 0.03, duration: 0.3, ease: "power2.out" });
});
link.addEventListener("mouseleave", () => {
gsap.to(spans, { color: "#fff", stagger: 0.03, duration: 0.3, ease: "power2.in" });
});
});
Notes
Desktop & Tablet only: Features that use cursor movement or hover states.
All devices: Smooth scrolling, text split, and scroll-triggered animations.