What GSAP is (and why it's famous)
GSAP (GreenSock Animation Platform) is a JavaScript library for animating anything — HTML, SVG, canvas, WebGL, React props, even arbitrary JS values. It's famous because it's the fastest, most reliable animation tool on the web, with an API that reads like plain English.
CSS animations vs. GSAP — when to reach for each
Use CSS when
- Simple hover, focus, or state transitions
- Animation runs once and doesn't need sequencing
- You want zero JavaScript overhead
- The animation is tied to user interaction (hover, focus)
Use GSAP when
- You need to sequence multiple animations (timeline)
- Animations depend on scroll position
- You need to animate SVG morphs, paths, or 3D transforms smoothly
- You want pause, reverse, restart controls
- You're staggering lots of elements
- Performance & consistency across browsers matters
Installing GSAP — one line
The CDN is the easiest way to start. Paste one script tag, and gsap
is available globally.
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script>
gsap.to('.box', { x: 200, duration: 1 });
</script>npm install gsap
// then in your JS
import gsap from 'gsap';
gsap.to('.box', { x: 200, duration: 1 });<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script> <script> gsap.registerPlugin(ScrollTrigger); </script>
gsap.to() — the one method you'll use most
Click the button to animate the box. Click it again to watch it re-run.
gsap.to() means "animate these elements to these
values over this duration."
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0; min-height: 100vh;
display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 24px;
background: #0a0a0a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-stage {
background: rgba(255,255,255,.03);
border: 1px solid rgba(255,255,255,.08);
border-radius: 12px; padding: 28px;
min-height: 200px; width: 320px;
display: flex; align-items: center; justify-content: center;
overflow: hidden;
}
.box {
width: 70px; height: 70px; border-radius: 14px;
background: linear-gradient(135deg, #10b981, #8b5cf6);
box-shadow: 0 10px 24px rgba(16,185,129,.3);
}
.demo-btn {
background: #10b981; color: #0a0a0a; font-weight: 700;
padding: 10px 18px; border-radius: 10px; border: none;
font-family: inherit; font-size: 0.9rem; cursor: pointer;
}
</style>
</head>
<body>
<div class="demo-stage">
<div class="box" id="demo1-box"></div>
</div>
<button class="demo-btn" onclick="animate()">Run animation</button>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script>
function animate() {
gsap.killTweensOf('#demo1-box');
gsap.set('#demo1-box', {
x: 0, rotation: 0, scale: 1,
background: 'linear-gradient(135deg, #10b981, #8b5cf6)'
});
gsap.to('#demo1-box', {
x: 180, // move 180px to the right
rotation: 360, // spin once
scale: 1.4, // grow 40%
backgroundColor: '#ec4899',
duration: 1, // over 1 second
ease: 'power2.out', // smooth deceleration
yoyo: true, // come back
repeat: 1 // once is enough
});
}
</script>
</body>
</html>// Targets the box and runs it on click. Paste the HTML tab into a
// blank file to see this animate exactly like the demo above.
function animate() {
// wipe any in-progress run, then reset to the starting look
gsap.killTweensOf('#demo1-box');
gsap.set('#demo1-box', {
x: 0, rotation: 0, scale: 1,
background: 'linear-gradient(135deg, #10b981, #8b5cf6)'
});
gsap.to('#demo1-box', {
x: 180, // move 180px to the right
rotation: 360, // spin once
scale: 1.4, // grow 40%
backgroundColor: '#ec4899',
duration: 1, // over 1 second
ease: 'power2.out', // smooth deceleration
yoyo: true, // come back
repeat: 1 // once is enough
});
}Stagger — animate lists with one line
stagger tells GSAP to delay each element by a bit, giving you
a cascading effect for free. This would be tedious in raw CSS.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0; min-height: 100vh;
display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 24px;
background: #0a0a0a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-stage {
background: rgba(255,255,255,.03);
border: 1px solid rgba(255,255,255,.08);
border-radius: 12px; padding: 28px;
min-height: 140px; width: 320px;
display: flex; align-items: center; justify-content: center;
overflow: hidden;
}
.stagger-grid {
display: flex; gap: 14px; flex-wrap: wrap; justify-content: center;
}
.stagger-grid .box {
width: 60px; height: 60px; border-radius: 12px;
opacity: 0; transform: translateY(20px);
}
/* six distinct gradients so each box reads as its own color */
.stagger-grid .box:nth-child(1) { background: linear-gradient(135deg, #ef4444, #f97316); }
.stagger-grid .box:nth-child(2) { background: linear-gradient(135deg, #f59e0b, #fbbf24); }
.stagger-grid .box:nth-child(3) { background: linear-gradient(135deg, #10b981, #06b6d4); }
.stagger-grid .box:nth-child(4) { background: linear-gradient(135deg, #3b82f6, #6366f1); }
.stagger-grid .box:nth-child(5) { background: linear-gradient(135deg, #8b5cf6, #a855f7); }
.stagger-grid .box:nth-child(6) { background: linear-gradient(135deg, #ec4899, #f43f5e); }
.demo-btn {
background: #10b981; color: #0a0a0a; font-weight: 700;
padding: 10px 18px; border-radius: 10px; border: none;
font-family: inherit; font-size: 0.9rem; cursor: pointer;
}
</style>
</head>
<body>
<div class="demo-stage">
<div class="stagger-grid" id="staggerGrid">
<div class="box"></div><div class="box"></div><div class="box"></div>
<div class="box"></div><div class="box"></div><div class="box"></div>
</div>
</div>
<button class="demo-btn" onclick="runStagger()">Run stagger</button>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script>
function runStagger() {
gsap.killTweensOf('#staggerGrid .box');
gsap.set('#staggerGrid .box', { y: 20, opacity: 0, scale: 0.8 });
gsap.to('#staggerGrid .box', {
y: 0, opacity: 1, scale: 1,
duration: 0.6,
ease: 'back.out(1.7)',
stagger: 0.08 // each box starts 80ms after the previous
});
}
// play it once on load, like the demo
window.addEventListener('load', runStagger);
</script>
</body>
</html>// Matches the demo: reset the six boxes, then cascade them in.
function runStagger() {
gsap.killTweensOf('#staggerGrid .box');
gsap.set('#staggerGrid .box', { y: 20, opacity: 0, scale: 0.8 });
gsap.to('#staggerGrid .box', {
y: 0, opacity: 1, scale: 1,
duration: 0.6,
ease: 'back.out(1.7)',
stagger: 0.08 // each box starts 80ms after the previous
});
}
window.addEventListener('load', runStagger);
// Stagger supports more — for a 2D grid you can ripple from the center:
gsap.to('.grid-item', {
scale: 1,
stagger: {
each: 0.05,
from: 'center', // 'start' | 'end' | 'center' | 'edges' | 'random'
grid: [6, 6] // treat as a 6×6 grid
}
});Timelines — choreograph multiple animations
A gsap.timeline() lets you sequence animations one after
another, or overlap them, without calculating delays by hand. This is where GSAP really
pulls ahead of raw CSS.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0; min-height: 100vh;
display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 24px;
background: #0a0a0a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-stage {
background: rgba(255,255,255,.03);
border: 1px solid rgba(255,255,255,.08);
border-radius: 12px; padding: 28px;
min-height: 220px; width: 360px;
display: flex; align-items: center; justify-content: center;
overflow: hidden;
}
.timeline-stage { width: 100%; position: relative; height: 180px; }
.tl-box {
position: absolute; width: 60px; height: 60px; border-radius: 12px;
top: 60px; left: 30px;
background: linear-gradient(135deg, #10b981, #8b5cf6);
}
.tl-text {
position: absolute; bottom: 10px; left: 0; right: 0;
text-align: center; font-size: 1.3rem; font-weight: 700;
color: #10b981; opacity: 0;
}
.demo-btn {
background: #10b981; color: #0a0a0a; font-weight: 700;
padding: 10px 18px; border-radius: 10px; border: none;
font-family: inherit; font-size: 0.9rem; cursor: pointer;
}
</style>
</head>
<body>
<div class="demo-stage">
<div class="timeline-stage">
<div class="tl-box" id="tlBox"></div>
<div class="tl-text" id="tlText">Done! 🎉</div>
</div>
</div>
<button class="demo-btn" onclick="playTimeline()">Play timeline</button>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script>
let tl;
function playTimeline() {
if (tl) tl.kill();
gsap.set('#tlBox', { x: 0, y: 0, rotation: 0 });
gsap.set('#tlText', { opacity: 0, y: 0 });
tl = gsap.timeline();
tl.to('#tlBox', { x: 250, duration: 0.8, ease: 'power2.inOut' })
.to('#tlBox', { rotation: 360, duration: 0.6 })
.to('#tlBox', { y: -40, duration: 0.3, yoyo: true, repeat: 1 })
.to('#tlBox', { x: 0, duration: 0.6, ease: 'power2.inOut' })
.to('#tlText', { opacity: 1, y: -10, duration: 0.4 }, '-=0.2');
}
</script>
</body>
</html>// Matches the demo above (ids #tlBox and #tlText).
let tl;
function playTimeline() {
if (tl) tl.kill(); // restart cleanly each click
gsap.set('#tlBox', { x: 0, y: 0, rotation: 0 });
gsap.set('#tlText', { opacity: 0, y: 0 });
tl = gsap.timeline();
tl.to('#tlBox', { x: 250, duration: 0.8, ease: 'power2.inOut' })
.to('#tlBox', { rotation: 360, duration: 0.6 })
.to('#tlBox', { y: -40, duration: 0.3, yoyo: true, repeat: 1 })
.to('#tlBox', { x: 0, duration: 0.6, ease: 'power2.inOut' })
.to('#tlText', { opacity: 1, y: -10, duration: 0.4 }, '-=0.2');
}
// The '-=0.2' on the last line overlaps the previous tween by 0.2s
// (it starts 0.2s before the previous animation ends).
// Timelines expose controls you can call anytime:
// tl.pause();
// tl.reverse();
// tl.restart();
// tl.progress(0.5); // jump to 50%Animating text, letter by letter
Split a string into characters, then animate each one with stagger.
This is how every award-winning hero section works.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0; min-height: 100vh;
display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 24px;
background: #0a0a0a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.demo-stage {
background: rgba(255,255,255,.03);
border: 1px solid rgba(255,255,255,.08);
border-radius: 12px; padding: 28px;
min-height: 160px; width: 360px;
display: flex; align-items: center; justify-content: center;
overflow: hidden;
}
.split-text {
font-size: clamp(1.6rem, 4vw, 2.6rem); font-weight: 800;
text-align: center; letter-spacing: -0.02em;
}
.split-text .char {
display: inline-block;
background: linear-gradient(135deg, #10b981, #8b5cf6);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.demo-btn {
background: #10b981; color: #0a0a0a; font-weight: 700;
padding: 10px 18px; border-radius: 10px; border: none;
font-family: inherit; font-size: 0.9rem; cursor: pointer;
}
</style>
</head>
<body>
<div class="demo-stage">
<div class="split-text" id="splitText"></div>
</div>
<button class="demo-btn" onclick="animateText()">Animate text</button>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script>
function animateText() {
const target = document.getElementById('splitText');
const text = 'Hello, animation.';
// Wrap each character in its own span (spaces become )
target.innerHTML = text
.split('')
.map(c => c === ' '
? ' '
: `<span class="char">${c}</span>`
).join('');
gsap.from('#splitText .char', {
y: 40,
opacity: 0,
rotation: 12,
duration: 0.6,
ease: 'back.out(2)',
stagger: 0.04
});
}
// run once on load, like the demo
window.addEventListener('load', animateText);
</script>
</body>
</html>// Matches the demo above (target element id="splitText").
function animateText() {
const target = document.getElementById('splitText');
const text = 'Hello, animation.';
// Wrap each character in its own span (spaces become )
target.innerHTML = text
.split('')
.map(c => c === ' '
? ' '
: `<span class="char">${c}</span>`
).join('');
gsap.from('#splitText .char', {
y: 40,
opacity: 0,
rotation: 12,
duration: 0.6,
ease: 'back.out(2)',
stagger: 0.04
});
}
window.addEventListener('load', animateText);ScrollTrigger — animate as the user scrolls
ScrollTrigger is GSAP's most famous plugin. It fires animations tied to scroll position — fade in on enter, pin elements, progress bars, parallax. Scroll down to see it fire.
This box appears when you scroll to it
When 80% of the viewport passes this element, GSAP fades it in, rotates it, and scales it up. If you scroll back past it, it resets.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0;
background: #0a0a0a; color: #f5f5f7;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* tall spacer so there is room to scroll down to the demo */
.spacer { height: 100vh; display: flex; align-items: center; justify-content: center; color: #555; }
.scroll-section {
min-height: 320px; max-width: 720px; margin: 0 auto; padding: 30px 24px;
display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: center;
}
@media (max-width: 680px) { .scroll-section { grid-template-columns: 1fr; } }
.scroll-box {
width: 100%; aspect-ratio: 1; max-width: 220px;
background: linear-gradient(135deg, #10b981, #8b5cf6);
border-radius: 20px; margin: 0 auto;
}
.scroll-text h3 { font-size: 1.3rem; margin-bottom: 8px; color: #f5f5f7; }
.scroll-text p { color: #a1a1aa; font-size: 0.95rem; line-height: 1.6; }
</style>
</head>
<body>
<div class="spacer">Scroll down ↓</div>
<div class="scroll-section">
<div class="scroll-box" id="scrollBox"></div>
<div class="scroll-text" id="scrollText">
<h3>This box appears when you scroll to it</h3>
<p>When 80% of the viewport passes this element, GSAP fades it in, rotates it, and scales it up. If you scroll back past it, it resets.</p>
</div>
</div>
<div class="spacer">keep scrolling</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
<script>
gsap.registerPlugin(ScrollTrigger);
gsap.from('#scrollBox', {
scale: 0.2, rotation: -180, opacity: 0,
duration: 1, ease: 'power3.out',
scrollTrigger: {
trigger: '#scrollBox',
start: 'top 80%', // fires when top of box hits 80% down viewport
toggleActions: 'play none none reverse'
// actions for: onEnter | onLeave | onEnterBack | onLeaveBack
}
});
gsap.from('#scrollText', {
x: 80, opacity: 0, duration: 1,
scrollTrigger: {
trigger: '#scrollText',
start: 'top 80%',
toggleActions: 'play none none reverse'
}
});
</script>
</body>
</html>// Requires both gsap.min.js and ScrollTrigger.min.js (see HTML tab).
// Matches the demo above (ids #scrollBox and #scrollText).
gsap.registerPlugin(ScrollTrigger);
gsap.from('#scrollBox', {
scale: 0.2,
rotation: -180,
opacity: 0,
duration: 1,
ease: 'power3.out',
scrollTrigger: {
trigger: '#scrollBox',
start: 'top 80%', // fires when top of box hits 80% down viewport
toggleActions: 'play none none reverse'
// actions for: onEnter | onLeave | onEnterBack | onLeaveBack
}
});
gsap.from('#scrollText', {
x: 80,
opacity: 0,
duration: 1,
scrollTrigger: {
trigger: '#scrollText',
start: 'top 80%',
toggleActions: 'play none none reverse'
}
});scrub: true to make the animation
scrub with the scrollbar, not just trigger once. That's how scroll-driven video-like
storytelling works.
Easing — the soul of an animation
The same animation with a different ease can feel completely
different. GSAP ships with dozens of curves — here are the ones you'll actually use:
// Linear — straight line. Rarely looks natural. ease: 'none' // Power (most common) — accelerates or decelerates ease: 'power1.out' // subtle ease: 'power2.out' // moderate (default) ease: 'power3.out' // strong ease: 'power4.out' // dramatic // Back — overshoots the target ease: 'back.out(1.7)' // classic "bounce past then settle" // Elastic — wobbles around the target ease: 'elastic.out(1, 0.3)' // Bounce — physical ball bounce ease: 'bounce.out' // Custom — your own curve ease: 'cubic-bezier(.17,.67,.83,.67)' // Browse all at: greensock.com/ease-visualizer
The whole GSAP core in 30 lines
// Animate TO these values
gsap.to('.box', { x: 200, duration: 1 });
// Animate FROM these values (to the CSS default)
gsap.from('.box', { y: -80, opacity: 0, duration: 0.6 });
// Animate FROM and TO (both ends specified)
gsap.fromTo('.box',
{ opacity: 0, y: 20 },
{ opacity: 1, y: 0, duration: 0.8 }
);
// Set values instantly (no tween)
gsap.set('.box', { scale: 1.2 });
// Kill all running animations on an element
gsap.killTweensOf('.box');
// Timeline — sequence animations
const tl = gsap.timeline({ repeat: -1, yoyo: true });
tl.to('.a', { x: 200 })
.to('.b', { y: 100 }, '<') // start with previous
.to('.c', { rotation: 90 }, '+=0.5'); // 0.5s after previous ends
// Stagger across a group
gsap.to('.item', { opacity: 1, stagger: 0.05 });
// ScrollTrigger — tie to scroll
gsap.to('.hero', {
y: 100,
scrollTrigger: {
trigger: '.hero',
start: 'top top',
end: 'bottom top',
scrub: true
}
});