GSAP ScrollTrigger parallax
The industry-standard way: ScrollTrigger scrubs a tween across the scroll range, so images drift and headings move with perfect smoothing.
JavaScriptGSAPIntermediate
<div class="space">Scroll down</div>
<div class="col">
<h2 class="head">Images drift,<br>headings float</h2>
<div class="frame"><div class="img i1"></div></div>
<h2 class="head">Scrubbed to<br>the scroll</h2>
<div class="frame"><div class="img i2"></div></div>
<div class="frame"><div class="img i3"></div></div>
</div>
body{display:block;height:auto}
.space{height:50vh;display:grid;place-items:center;color:#a6938f;font-size:14px}
.col{width:min(440px,88vw);margin:0 auto;display:grid;gap:60px;padding-bottom:80px}
.frame{position:relative;height:240px;overflow:hidden;border-radius:16px}
.img{position:absolute;left:0;right:0;top:-20%;height:140%}
.i1{background:radial-gradient(circle at 70% 35%,#ffd166 0 7%,transparent 8%),linear-gradient(170deg,#e5603a,#8a3a22 70%)}
.i2{background:radial-gradient(circle at 30% 55%,#f08a5d 0 6%,transparent 7%),linear-gradient(170deg,#6e2a18,#2a0f0b)}
.i3{background:radial-gradient(circle at 60% 45%,#fff 0 5%,transparent 6%),linear-gradient(170deg,#d98a8a,#4e1408)}
h2{margin:0;font-size:clamp(2rem,8vw,3rem);letter-spacing:-.03em}
if (window.gsap) {
gsap.registerPlugin(ScrollTrigger);
// Images move inside their frames
gsap.utils.toArray('.frame').forEach(frame => {
gsap.fromTo(frame.querySelector('.img'),
{ yPercent: -14 },
{ yPercent: 14, ease: 'none',
scrollTrigger: { trigger: frame, start: 'top bottom', end: 'bottom top', scrub: true } });
});
// Headings drift a little faster than the page
gsap.utils.toArray('.head').forEach(h => {
gsap.to(h, { y: -50, ease: 'none',
scrollTrigger: { trigger: h, start: 'top bottom', end: 'bottom top', scrub: 0.6 } });
});
}
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GSAP ScrollTrigger parallax</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{display:grid;place-items:center;background:#140807;color:#fffffc;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
body{display:block;place-items:normal;height:auto}
.space{height:50vh;display:grid;place-items:center;color:#a6938f;font-size:14px}
.col{width:min(440px,88vw);margin:0 auto;display:grid;gap:60px;padding-bottom:80px}
.frame{position:relative;height:240px;overflow:hidden;border-radius:16px}
.img{position:absolute;left:0;right:0;top:-20%;height:140%}
.i1{background:radial-gradient(circle at 70% 35%,#ffd166 0 7%,transparent 8%),linear-gradient(170deg,#e5603a,#8a3a22 70%)}
.i2{background:radial-gradient(circle at 30% 55%,#f08a5d 0 6%,transparent 7%),linear-gradient(170deg,#6e2a18,#2a0f0b)}
.i3{background:radial-gradient(circle at 60% 45%,#fff 0 5%,transparent 6%),linear-gradient(170deg,#d98a8a,#4e1408)}
h2{margin:0;font-size:clamp(2rem,8vw,3rem);letter-spacing:-.03em}
</style>
<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>
</head>
<body>
<div class="space">Scroll down</div>
<div class="col">
<h2 class="head">Images drift,<br>headings float</h2>
<div class="frame"><div class="img i1"></div></div>
<h2 class="head">Scrubbed to<br>the scroll</h2>
<div class="frame"><div class="img i2"></div></div>
<div class="frame"><div class="img i3"></div></div>
</div>
<script>
if (window.gsap) {
gsap.registerPlugin(ScrollTrigger);
// Images move inside their frames
gsap.utils.toArray('.frame').forEach(frame => {
gsap.fromTo(frame.querySelector('.img'),
{ yPercent: -14 },
{ yPercent: 14, ease: 'none',
scrollTrigger: { trigger: frame, start: 'top bottom', end: 'bottom top', scrub: true } });
});
// Headings drift a little faster than the page
gsap.utils.toArray('.head').forEach(h => {
gsap.to(h, { y: -50, ease: 'none',
scrollTrigger: { trigger: h, start: 'top bottom', end: 'bottom top', scrub: 0.6 } });
});
}
</script>
</body></html>
Uses: https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js, https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js
Good for
Production sites already using GSAP, or any time you need pinning, scrubbing and callbacks to work together.
Watch out for
Register the plugin before use, and set ease: none on scrubbed tweens so motion matches the scroll one to one. The page still works if the CDN fails, so guard for window.gsap.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.