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,...