Skip to content

Animations with GSAP

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.


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

GSAP and its plugins are loaded in Project.js via the AssetManager:

// In extraAssets.js - GSAP is defined as a Terra internal library
export 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;
},
},
];
};

Once loaded, GSAP plugins are registered in Project.js:

src/scripts/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 plugins
this.GSAPLIB.gsap.registerPlugin(
this.Manager.getLibrary("ScrollTrigger"),
this.Manager.getLibrary("Flip")
);
// Store reference for easy access
this.gsap = this.GSAPLIB.gsap;

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 in NETWRIX coordinate three animation systems:

  1. Out Animation - Fade/hide current page content
  2. In Animation - Reveal new page content
  3. Content Animations - Heroes, reveals, and scroll-based effects

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 functions

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,
}));
},
},
];
};

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;
}
}

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 preloaderIn animation runs only on initial page load (not on SWUP transitions) to:

  1. Hide the loading screen
  2. Reveal the first page’s content
  3. Coordinate with hero and reveal animations
// In Project.js init() method
const 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 animation
const preloaderIn = new In({
element: this.DOM.preloader,
spinner: null,
Manager: this.Manager,
});
tl.add(preloaderIn).add("preloaderFinished");
// 2. Hero animations follow
if (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 viewport
if (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
}
});
}

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.


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 ScrollTrigger
Page 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 ScrollTrigger

In Project.js (initial load):

const tl = gsap.timeline();
// Step 1: Preloader
tl.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 are page-specific entrance effects located in src/scripts/motion/hero/:

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;

Heroes are loaded on-demand via AssetManager:

// In extraAssets.js
export 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;
},
},
];
};

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;
}
}

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;
}
}

“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)

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
},
});

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,
});

  1. Use timelines for sequences

    const tl = gsap.timeline();
    tl.to(element1, { opacity: 1 })
    .to(element2, { opacity: 1 }, "-=0.3"); // Overlap by 0.3s
  2. Set initial states explicitly

    gsap.set(elements, { opacity: 0, y: 20 });
    gsap.to(elements, { opacity: 1, y: 0 });
  3. Use ease functions appropriately

    // Smooth entrances
    { ease: "power3.out" }
    // Snappy interactions
    { ease: "power2.inOut" }
    // Elastic bounces
    { ease: "elastic.out(1, 0.3)" }
  4. Leverage stagger for groups

    gsap.to(elements, {
    opacity: 1,
    stagger: {
    each: 0.1,
    from: "start", // or "end", "center", "edges", "random"
    }
    });
  5. Clean up ScrollTriggers in destroy()

    destroy() {
    if (this.scrollTrigger) {
    this.scrollTrigger.kill();
    this.scrollTrigger = null;
    }
    }
  6. 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");
  7. Return timelines from animation classes

    init() {
    const tl = gsap.timeline();
    // ... animations
    return tl; // Allows external control
    }
  1. Don’t animate expensive properties

    // ❌ Causes repaints
    gsap.to(element, { width: "100%", height: "100%" });
    // ✅ Use transforms instead
    gsap.to(element, { scaleX: 1, scaleY: 1 });
  2. Don’t forget to kill timelines

  3. Don’t use too many ScrollTriggers

  4. Don’t mix units in transforms

  5. Don’t skip fromTo for complex states


gsap.fromTo(element,
{ opacity: 0, y: 30 },
{
opacity: 1,
y: 0,
duration: 0.6,
ease: "power3.out"
}
);
gsap.from(listItems, {
opacity: 0,
y: 20,
duration: 0.5,
stagger: 0.1,
ease: "power2.out"
});
gsap.fromTo(element,
{ opacity: 0, scale: 0.8 },
{
opacity: 1,
scale: 1,
duration: 0.7,
ease: "back.out(1.2)"
}
);
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");
gsap.to(background, {
scrollTrigger: {
trigger: container,
start: "top bottom",
end: "bottom top",
scrub: true,
},
y: -100,
ease: "none"
});

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:

  1. Initial Load: Preloader → Heroes → Above-fold reveals
  2. Page Transitions: Out → SWUP content swap → In → Heroes → Reveals
  3. 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.