Animations with GSAP
Introduction
Section titled “Introduction”The NETWRIX website uses GSAP (GreenSock Animation Platform) as the animation engine for all motion design. GSAP is the industry-leading animation library that delivers high-performance, timeline-based animations with precise control and cross-browser compatibility.
Combined with SWUP’s page transition system, GSAP enables smooth, coordinated animations that create a polished user experience without sacrificing performance.
Why GSAP?
Section titled “Why GSAP?”GSAP is our animation library of choice because:
- Performance - Hardware-accelerated, 60fps animations
- Timeline Control - Sequence animations with precise timing
- ScrollTrigger - Scroll-based animations out of the box
- Plugin Ecosystem - Extensive capabilities (Flip, MotionPath, etc.)
- Developer Experience - Intuitive API, extensive documentation
- Cross-Browser - Works consistently across all browsers
Official Documentation
Section titled “Official Documentation”- GSAP Core: https://greensock.com/docs/v3/GSAP
- ScrollTrigger: https://greensock.com/docs/v3/Plugins/ScrollTrigger
- Flip Plugin: https://greensock.com/docs/v3/Plugins/Flip
- Easing Visualizer: https://greensock.com/ease-visualizer
GSAP Integration in NETWRIX
Section titled “GSAP Integration in NETWRIX”Library Loading
Section titled “Library Loading”GSAP and its plugins are loaded in Project.js via the AssetManager:
// In extraAssets.js - GSAP is defined as a Terra internal libraryexport const getTerraInternal = () => { return [ { name: "GSAP", resource: async () => { const module = await import("gsap"); return { ...module }; // Contains gsap, timeline, etc. }, }, { name: "ScrollTrigger", resource: async () => { const { ScrollTrigger } = await import("gsap/ScrollTrigger"); return ScrollTrigger; }, }, { name: "Flip", resource: async () => { const { default: Flip } = await import("gsap/Flip"); return Flip; }, }, ];};Plugin Registration
Section titled “Plugin Registration”Once loaded, GSAP plugins are registered in Project.js:
this.GSAPLIB = this.Manager.getLibrary("GSAP");if (!this.GSAPLIB || !this.GSAPLIB.gsap) { throw new Error("GSAP library not found or not properly loaded");}
// Register pluginsthis.GSAPLIB.gsap.registerPlugin( this.Manager.getLibrary("ScrollTrigger"), this.Manager.getLibrary("Flip"));
// Store reference for easy accessthis.gsap = this.GSAPLIB.gsap;Accessing GSAP in Classes
Section titled “Accessing GSAP in Classes”GSAP is accessed via the Manager system:
class MyAnimation { constructor({ element, Manager }) { this.DOM = { element }; this.gsap = Manager.getLibrary("GSAP").gsap; this.init(); }
init() { this.gsap.from(this.DOM.element, { opacity: 0, duration: 0.6, }); }}Page Transitions with SWUP
Section titled “Page Transitions with SWUP”Overview
Section titled “Overview”Page transitions in NETWRIX coordinate three animation systems:
- Out Animation - Fade/hide current page content
- In Animation - Reveal new page content
- Content Animations - Heroes, reveals, and scroll-based effects
The Transition System
Section titled “The Transition System”Located in src/scripts/motion/transition/, the system consists of:
transition/├── index.js # SWUP transition configuration├── In.js # Fade-in transition├── Out.js # Fade-out transition└── utilities.js # Helper functionsTransition Configuration (index.js)
Section titled “Transition Configuration (index.js)”The createTransitionOptions function defines SWUP’s animation behavior:
export const createTransitionOptions = (payload) => { const { Manager, terraDebug, assetManager } = payload; const gsap = Manager.getLibrary("GSAP").gsap;
return [ { from: "(.*)", // Any page to: "(.*)", // To any page
// IN transition - plays when new page loads in: async (next, infos) => { const tl = gsap.timeline();
// 1. Play In animation (fade in transition element) tl.add(new In({ element: document.querySelector(".js--transition"), spinner: document.querySelector(".js--spinner-transition"), Manager: Manager, })).add("transitionFinished");
// 2. Play hero animations (if present) if (document.querySelector(".c--hero-a")) { const heroA = await assetManager.getAnimation("HeroA"); tl.add(new heroA({ element: document.querySelector(".c--hero-a"), Manager }), "transitionFinished"); }
// 3. Initialize reveal animations // ... },
// OUT transition - plays before leaving page out: (next, infos) => { const tl = gsap.timeline({ onComplete: async () => { await smoothScrollToTop(); // Scroll to top next(); // Tell SWUP to continue } });
// Play Out animation tl.add(new Out({ element: document.querySelector(".js--transition"), Manager: Manager, })); }, }, ];};In.js - Fade In Transition
Section titled “In.js - Fade In Transition”The In transition reveals the new page:
class In { constructor(payload) { this.DOM = { element: payload.element, // Transition overlay spinner: payload.spinner // Loading spinner }; this.gsap = payload.Manager.getLibrary("GSAP").gsap; return this.init(); }
init() { const tl = this.gsap.timeline({ defaults: { duration: 0.3, ease: "power4.in" }, onStart: () => { // Hide transition element this.gsap.to(this.DOM.element, { display: "none" }); }, onComplete: () => { // Clear inline styles this.gsap.set(this.DOM.element, { clearProps: "all" }); if (this.DOM.spinner) { this.gsap.set(this.DOM.spinner, { clearProps: "all" }); } } });
// Animate transition squares scaling out tl.add( useScaleAnimation({ element: this.DOM.element, gsap: this.gsap, options: { scale: 0, duration: 0.3, ease: "power4.in", stagger: { each: 0.01, from: "random" } } }) );
return tl; }}Out.js - Fade Out Transition
Section titled “Out.js - Fade Out Transition”The Out transition hides the current page:
class Out { constructor(payload) { this.DOM = { element: payload.element }; this.gsap = payload.Manager.getLibrary("GSAP").gsap; return this.init(); }
init() { const tl = this.gsap.timeline({ defaults: { duration: 0.3, ease: "power4.out" }, onStart: () => { // Show transition element this.gsap.to(this.DOM.element, { display: "grid" }); }, });
// Animate transition squares scaling in tl.add( useScaleAnimation({ element: this.DOM.element, gsap: this.gsap, options: { scale: 1, duration: 0.3, ease: "power4.out", stagger: { each: 0.01, from: "random" } } }) );
return tl; }}The Preloader Animation
Section titled “The Preloader Animation”Purpose
Section titled “Purpose”The preloaderIn animation runs only on initial page load (not on SWUP transitions) to:
- Hide the loading screen
- Reveal the first page’s content
- Coordinate with hero and reveal animations
Implementation in Project.js
Section titled “Implementation in Project.js”// In Project.js init() methodconst tl = this.gsap.timeline({ defaults: { duration: 0.1, ease: "power1.inOut", }, onStart: () => { // Trigger Boostify to load third-party scripts if (this.DOM.boostifyScripts.length > 0) { this.boostify.onload({ maxTime: 1200, callback: async () => { // Load GTM and other tracking scripts } }); } },});
// 1. Play preloader animationconst preloaderIn = new In({ element: this.DOM.preloader, spinner: null, Manager: this.Manager,});tl.add(preloaderIn).add("preloaderFinished");
// 2. Hero animations followif (this.DOM.heroA) { const HeroA = await this.assetManager.getAnimation("HeroA"); tl.add(new HeroA({ element: this.DOM.heroA, Manager: this.Manager }));}
// 3. Reveal animations for elements in viewportif (this.DOM.revealIt) { const RevealIt = await this.assetManager.getAnimation("RevealIt"); this.DOM.revealIt.forEach((element) => { if (Manager.libraries.isElementInViewport({ el: element })) { tl.add(new RevealIt({ play: "instant", container: element, // ... }), "preloaderFinished"); // Stack with preloader end } });}Why “preloaderFinished” Label?
Section titled “Why “preloaderFinished” Label?”The "preloaderFinished" label is a timeline position marker that allows animations to start simultaneously:
// Timeline sequence:tl.add(preloaderIn) // 0s - 0.3s .add("preloaderFinished") // Label at 0.3s .add(heroAnimation) // Starts at 0.3s .add(revealAnimation, "preloaderFinished"); // ALSO starts at 0.3s (stacked)Result: Hero and reveal animations play together after the preloader finishes.
Animation Stacking Strategy
Section titled “Animation Stacking Strategy”The Animation Sequence
Section titled “The Animation Sequence”Initial Page Load:1. Preloader hides (0.3s) ↓2. Hero animates in (parallel with step 3) ↓3. Above-fold RevealIt/RevealStack animate in ↓4. Below-fold animations initialize via ScrollTriggerPage Transition:1. Out animation plays (0.3s) ↓2. Smooth scroll to top ↓3. SWUP replaces content ↓4. In animation plays (0.3s) ↓5. Hero animates in (parallel with step 6) ↓6. Above-fold RevealIt/RevealStack animate in ↓7. Below-fold animations initialize via ScrollTriggerCoordinating Animations
Section titled “Coordinating Animations”In Project.js (initial load):
const tl = gsap.timeline();
// Step 1: Preloadertl.add(preloaderIn).add("preloaderFinished");
// Step 2: Hero (starts after preloader)if (this.DOM.heroA) { tl.add(heroAnimation);}
// Step 3: Above-fold reveals (stacked with hero)revealElements.forEach((element) => { if (isInViewport(element)) { tl.add(new RevealIt({ play: "instant", // ... }), "preloaderFinished"); // Starts with hero } else { // Below-fold: use ScrollTrigger Manager.addInstance("RevealIt", new RevealIt({ play: "onscroll", // ... })); }});In transition/index.js (page changes):
in: async (next, infos) => { const tl = gsap.timeline();
// Step 1: In transition tl.add(inAnimation).add("transitionFinished");
// Step 2: Hero (if present) if (document.querySelector(".c--hero-a")) { const heroA = await assetManager.getAnimation("HeroA"); tl.add(new heroA({ element, Manager }), "transitionFinished"); }
// Step 3: Above-fold reveals document.querySelectorAll(".js--reveal-it").forEach((element) => { if (isInViewport(element)) { tl.add(new RevealIt({ play: "instant", // ... }), "transitionFinished"); } });}Hero Animations
Section titled “Hero Animations”Hero animations are page-specific entrance effects located in src/scripts/motion/hero/:
HeroA Example
Section titled “HeroA Example”class HeroA { constructor({ element, Manager }) { this.DOM = { element: element, title: element.querySelector('.c--hero-a__hd__title'), description: element.querySelector('.c--hero-a__hd__description'), cta: element.querySelector('.c--hero-a__hd__cta'), }; this.gsap = Manager.getLibrary("GSAP").gsap; return this.init(); }
init() { const tl = this.gsap.timeline({ defaults: { duration: 0.8, ease: "power3.out" } });
// Set initial state this.gsap.set([this.DOM.title, this.DOM.description, this.DOM.cta], { opacity: 0, y: 30, });
// Animate in sequence tl.to(this.DOM.title, { opacity: 1, y: 0 }) .to(this.DOM.description, { opacity: 1, y: 0 }, "-=0.6") .to(this.DOM.cta, { opacity: 1, y: 0 }, "-=0.5");
return tl; }}
export default HeroA;Loading Hero Animations
Section titled “Loading Hero Animations”Heroes are loaded on-demand via AssetManager:
// In extraAssets.jsexport const getAnimations = () => { return [ { name: "HeroA", domElement: document.querySelector(".c--hero-a"), resource: async () => { const { default: HeroA } = await import("@scripts/motion/hero/HeroA"); return HeroA; }, }, { name: "HeroH", domElement: document.querySelector(".c--hero-h"), resource: async () => { const { default: HeroH } = await import("@scripts/motion/hero/HeroH"); return HeroH; }, }, ];};Reveal Animations
Section titled “Reveal Animations”RevealIt
Section titled “RevealIt”Purpose: Animates individual elements within a container (headers, paragraphs, buttons, images).
Strategy: Elements in viewport animate instantly; below-fold elements use ScrollTrigger.
class RevealIt { constructor(payload) { const { play, container, headers, textElements, clickableElements, mediaElements, Manager } = payload;
this.DOM = { container, headers, // h1, h2, h3, etc. textElements, // p, span, ul, ol clickableElements, // a, button, input mediaElements, // images, videos };
this.play = play; // "instant" or "onscroll" this.gsap = Manager.getLibrary("GSAP").gsap;
return this.init(); }
init() { const tl = this.gsap.timeline({ defaults: { duration: 0.6, ease: "power3.out" } });
// Set initial states this.gsap.set(this.DOM.headers, { opacity: 0, y: 20 }); this.gsap.set(this.DOM.textElements, { opacity: 0, y: 15 }); this.gsap.set(this.DOM.clickableElements, { opacity: 0 }); this.gsap.set(this.DOM.mediaElements, { opacity: 0, scale: 0.95 });
// Animate in sequence tl.to(this.DOM.headers, { opacity: 1, y: 0, stagger: 0.1 }) .to(this.DOM.textElements, { opacity: 1, y: 0, stagger: 0.05 }, "-=0.4") .to(this.DOM.clickableElements, { opacity: 1, stagger: 0.05 }, "-=0.3") .to(this.DOM.mediaElements, { opacity: 1, scale: 1, stagger: 0.1 }, "-=0.5");
// Configure based on play mode if (this.play === "onscroll") { tl.pause(); // Pause until scroll trigger
this.scrollTrigger = this.gsap.ScrollTrigger.create({ trigger: this.DOM.container, start: "top 80%", once: true, onEnter: () => tl.play(), }); }
return tl; }
destroy() { if (this.scrollTrigger) { this.scrollTrigger.kill(); this.scrollTrigger = null; } this.DOM = null; }}RevealStack
Section titled “RevealStack”Purpose: Animates all direct children of a container as a stacked sequence.
class RevealStack { constructor(payload) { const { play, container, childrenElements, Manager } = payload;
this.DOM = { container, children: Array.from(childrenElements), };
this.play = play; this.gsap = Manager.getLibrary("GSAP").gsap;
return this.init(); }
init() { const tl = this.gsap.timeline({ defaults: { duration: 0.6, ease: "power3.out" } });
// Set initial state this.gsap.set(this.DOM.children, { opacity: 0, y: 30, });
// Animate children in staggered sequence tl.to(this.DOM.children, { opacity: 1, y: 0, stagger: 0.15, });
// Configure based on play mode if (this.play === "onscroll") { tl.pause();
this.scrollTrigger = this.gsap.ScrollTrigger.create({ trigger: this.DOM.container, start: "top 75%", once: true, onEnter: () => tl.play(), }); }
return tl; }
destroy() { if (this.scrollTrigger) { this.scrollTrigger.kill(); this.scrollTrigger = null; } this.DOM = null; }}Why Two Play Modes?
Section titled “Why Two Play Modes?”“instant” mode (elements in viewport):
- Plays immediately when timeline reaches them
- Part of the coordinated preloader → hero → reveals sequence
- No ScrollTrigger needed
“onscroll” mode (elements below fold):
- Timeline is paused on creation
- ScrollTrigger monitors scroll position
- Plays when element enters viewport
- Conserves performance (doesn’t animate off-screen elements)
ScrollTrigger Integration
Section titled “ScrollTrigger Integration”Basic Usage
Section titled “Basic Usage”this.gsap.ScrollTrigger.create({ trigger: this.DOM.element, // Element to watch start: "top 80%", // When top of element is 80% down viewport end: "bottom 20%", // When bottom is 20% down viewport once: true, // Only fire once onEnter: () => { // Animation logic },});Common Patterns
Section titled “Common Patterns”Fade in on scroll:
this.gsap.to(this.DOM.element, { scrollTrigger: { trigger: this.DOM.element, start: "top 80%", once: true, }, opacity: 1, y: 0, duration: 0.6,});Pin element while scrolling:
this.gsap.ScrollTrigger.create({ trigger: this.DOM.container, start: "top top", end: "bottom bottom", pin: this.DOM.stickyElement, pinSpacing: false,});Scrub animation with scroll progress:
this.gsap.to(this.DOM.element, { scrollTrigger: { trigger: this.DOM.container, start: "top center", end: "bottom center", scrub: 1, // Smooth scrubbing (1 second delay) }, x: 100, rotation: 360,});Animation Best Practices
Section titled “Animation Best Practices”✅ Do’s
Section titled “✅ Do’s”-
Use timelines for sequences
const tl = gsap.timeline();tl.to(element1, { opacity: 1 }).to(element2, { opacity: 1 }, "-=0.3"); // Overlap by 0.3s -
Set initial states explicitly
gsap.set(elements, { opacity: 0, y: 20 });gsap.to(elements, { opacity: 1, y: 0 }); -
Use ease functions appropriately
// Smooth entrances{ ease: "power3.out" }// Snappy interactions{ ease: "power2.inOut" }// Elastic bounces{ ease: "elastic.out(1, 0.3)" } -
Leverage stagger for groups
gsap.to(elements, {opacity: 1,stagger: {each: 0.1,from: "start", // or "end", "center", "edges", "random"}}); -
Clean up ScrollTriggers in destroy()
destroy() {if (this.scrollTrigger) {this.scrollTrigger.kill();this.scrollTrigger = null;}} -
Use labels for complex timelines
tl.add("start").to(element1, { opacity: 1 }).add("middle").to(element2, { opacity: 1 }, "middle") // Start at "middle" label.add("end"); -
Return timelines from animation classes
init() {const tl = gsap.timeline();// ... animationsreturn tl; // Allows external control}
❌ Don’ts
Section titled “❌ Don’ts”-
Don’t animate expensive properties
// ❌ Causes repaintsgsap.to(element, { width: "100%", height: "100%" });// ✅ Use transforms insteadgsap.to(element, { scaleX: 1, scaleY: 1 }); -
Don’t forget to kill timelines
-
Don’t use too many ScrollTriggers
-
Don’t mix units in transforms
-
Don’t skip fromTo for complex states
Common Animation Patterns
Section titled “Common Animation Patterns”Pattern 1: Fade In & Slide Up
Section titled “Pattern 1: Fade In & Slide Up”gsap.fromTo(element, { opacity: 0, y: 30 }, { opacity: 1, y: 0, duration: 0.6, ease: "power3.out" });Pattern 2: Staggered List
Section titled “Pattern 2: Staggered List”gsap.from(listItems, { opacity: 0, y: 20, duration: 0.5, stagger: 0.1, ease: "power2.out"});Pattern 3: Scale & Fade
Section titled “Pattern 3: Scale & Fade”gsap.fromTo(element, { opacity: 0, scale: 0.8 }, { opacity: 1, scale: 1, duration: 0.7, ease: "back.out(1.2)" });Pattern 4: Progressive Reveal
Section titled “Pattern 4: Progressive Reveal”const tl = gsap.timeline();
tl.from(header, { opacity: 0, y: -20, duration: 0.5 }) .from(content, { opacity: 0, duration: 0.6 }, "-=0.3") .from(cta, { opacity: 0, scale: 0.9, duration: 0.4 }, "-=0.4");Pattern 5: Scroll-Based Parallax
Section titled “Pattern 5: Scroll-Based Parallax”gsap.to(background, { scrollTrigger: { trigger: container, start: "top bottom", end: "bottom top", scrub: true, }, y: -100, ease: "none"});Summary
Section titled “Summary”The NETWRIX animation system combines:
- GSAP for performant, timeline-based animations
- SWUP for seamless page transitions
- ScrollTrigger for scroll-based reveals
- AssetManager for code-split animation loading
Animation Flow:
- Initial Load: Preloader → Heroes → Above-fold reveals
- Page Transitions: Out → SWUP content swap → In → Heroes → Reveals
- Below-Fold: ScrollTrigger handles lazy animation initialization
Key Concepts:
- Use
"instant"play mode for visible elements - Use
"onscroll"play mode for below-fold elements - Stack animations using timeline labels (
"preloaderFinished","transitionFinished") - Always clean up timelines and ScrollTriggers in
destroy() - Leverage GSAP’s stagger and ease functions for polish
By understanding this coordinated system, you can create smooth, performant animations that enhance the user experience without blocking content delivery.
Related Topics
Section titled “Related Topics”- 🎯 Classes Structure - Animation class patterns
- 🔄 Handlers System - Lifecycle management
- 📦 Libraries - GSAP and animation utilities
- 📚 Framework Overview - Overall architecture