GSAP - ScrollTrigger

by Hugo Vale Pereira

HTML

<!--Tutorial from: https://www.youtube.com/watch?v=Gy9IS0Y6dy0  Nov 2025
inspired from: https://www.wildyriftian.com/works
-->

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ScrollTrigger.min.js"></script>
<nav>
  <p>Tutorial</p>
  <p>GSAP</p>
</nav>


<div class="container-3">
    <div class="box"><p>1</p></div>
  <div class="box"><p>2</p></div>
  <div class="box"><p>3</p></div>
  <div class="box"><p>4</p></div>
  <div class="box"><p>1</p></div>
  <div class="box"><p>2</p></div>
  <div class="box"><p>3</p></div>
  <div class="box"><p>4</p></div>
</div>

<div class="container container-1">
  <div class="box">1</div>
  <div class="box">2</div>
  <div class="box">3</div>
  <div class="box">4</div>
  <div class="box">1</div>
  <div class="box">2</div>
  <div class="box">3</div>
  <div class="box">4</div>
</div>


<div class="container container-2">
  <div class="box">1</div>
  <div class="box">2</div>
  <div class="box">3</div>
  <div class="box">4</div>
  <div class="box">1</div>
  <div class="box">2</div>
  <div class="box">3</div>
  <div class="box">4</div>
</div>

CSS

@import url("https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap");

:root {
  --bg: #dddddd;
  --fg: #1d1d1d;
  --variant-1: oklch(0.9412 0.2245 111.18);
  --variant-2: oklch(0.6647 0.2695 31.76);
  --variant-3: #bdbdbd;
  --disabled-folder-bg: #d2d2d2;
  --disabled-folder-fg: #8a8a8a;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

* {
  outline: #1d1d1d 1px dashed;
}

body {
  background: var(--bg);
  color: var(--fg);
}

h1 {
  font-family:
    "DM Sans",
    -apple-system,
    BlinkMacSystemFont,
    Segoe UI,
    Roboto,
    Helvetica,
    Arial,
    sans-serif;
  font-size: 2.75rem;
  font-weight: 400;
}

p {
  text-transform: uppercase;
  font-family: "DM Mono";
  font-size: 0.8rem;
  font-weight: 500;
}

h1,
p {
  transition: color 250ms ease;
}

nav {
  position: absolute;
  top: 0;
  width: 100%;
  padding: 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.container {
  margin-top: 20vh;
  padding-left: 51%;
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  gap: 1rem;
}

.container-3{
  margin-top: 20vh;
  height: 100px;
  overflow: auto;
  
}

.box {
  flex-shrink: 0;
  height: 100px;
  width: 100px;
  background: var(--variant-1);

 
}

.container {
  -webkit-overflow-scrolling: touch; /* Smooth scroll on ios */
  scrollbar-width: thin;
}

JavaScript

gsap.registerPlugin(ScrollTrigger);

const boxes1 = gsap.utils.toArray('.container-1 .box');

const boxes2 = gsap.utils.toArray('.container-2 .box');

boxes1.forEach((box) => {
  const tl = gsap.timeline({
    scrollTrigger: {
      trigger: box,
      horizontal: true,
      scroller: '.container-1',
      start: 'left center', // lado esquerdo toca o centro
      end: 'right center', // lado direito toca o centro
      scrub: true, // sincroniza com o scroll
      markers: true,
    },
  });

  tl.to(box, { y: -50 }) // sobe instantaneamente no start
    .to(box, { y: -50 }) // mantém em cima
    .to(box, { y: 0 }); // desce instantaneamente no end
});

boxes2.forEach((box) => {
  gsap.to(box, {
    scrollTrigger: {
      trigger: box,
      horizontal: true,
      scroller: '.container-2',

      // markers: false, // para debug - remove depois
    }, // start animation when ".box" enters the viewport
    y: -50,
  });
});

let tl = gsap.timeline({
  // yes, we can add it to an entire timeline!
  scrollTrigger: {
    trigger: '.container-3 .box',
    scroller: '.container-3',
    pin: true, // pin the trigger element while active
    start: 'top top', // when the top of the trigger hits the top of the viewport
    end: '+=500', // end after scrolling 500px beyond the start
    scrub: 1, // smooth scrubbing, takes 1 second to "catch up" to the scrollbar
    snap: {
      snapTo: 'labelsDirectional', // snap to the closest label in the timeline
      duration: { min: 0.2, max: 0.5 }, // the snap animation should be at least 0.2 seconds, but no more than 3 seconds (determined by velocity)
      delay: 0, // wait 0.2 seconds from the last scroll event before doing the snapping
      ease: 'power1.inOut', // the ease of the snap animation ("power3" by default)
      inertia: false,
    },
  },
});

// add animations and labels to the timeline
tl.addLabel('start')
  .from('.container-3 .box p', { scale: 0.3,...