ScrollTrigger Interactions
ScrollTrigger turns the scroll position into an animation timeline. As the user scrolls, elements fade in, parallax layers shift, and pinned sections unfold stories. With Claude Code, you describe what you see happening as you scroll — and it writes the GSAP for you.
The key prompting pattern
Describe the visual behavior as you experience it scrolling down the page. Don’t say “add a fade”; say “when this section enters the viewport, the cards appear one by one from the bottom.”
Always include this line in your prompt:
gsap.registerPlugin(ScrollTrigger)을 파일 상단에 추가해줘.
Without it, ScrollTrigger won’t activate and nothing will work.
Fade-in on scroll
The simplest use: elements appear as you reach them.
Prompt:
".card 요소들이 스크롤하다가 화면에 들어올 때 아래에서 올라오며 fade in되게 해줘.
start는 'top 80%', stagger 0.1s, ease power2.out.
gsap.registerPlugin(ScrollTrigger)을 상단에 추가해줘."
Claude Code output:
gsap.registerPlugin(ScrollTrigger);
gsap.from('.card', {
scrollTrigger: {
trigger: '.card-grid',
start: 'top 80%',
},
y: 32,
opacity: 0,
duration: 0.6,
stagger: 0.1,
ease: 'power2.out',
});
Parallax with scrub
scrub: true ties the animation directly to scroll position — the animation plays forward as you scroll down and reverses as you scroll back up.
Prompt:
".hero-text가 스크롤할 때 배경보다 느리게 움직이는 parallax 효과를 줘.
y: -80, scrub: 1로 설정해줘."
Claude Code output:
gsap.to('.hero-text', {
scrollTrigger: {
trigger: '.hero',
start: 'top top',
end: 'bottom top',
scrub: 1,
},
y: -80,
ease: 'none',
});
Pin + scrub for scroll storytelling
Pinning locks a section in place while you scroll through it, turning scroll distance into animation progress.
Prompt:
".story 섹션을 pin하고, 스크롤하는 동안 3개의 텍스트 패널이 순서대로
나타났다 사라지게 해줘. scrub: 1, pin: true."
Claude Code output:
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.story',
start: 'top top',
end: '+=200%',
pin: true,
scrub: 1,
},
});
tl.to('.panel-1', { opacity: 1, duration: 1 })
.to('.panel-1', { opacity: 0, duration: 0.5 })
.to('.panel-2', { opacity: 1, duration: 1 })
.to('.panel-2', { opacity: 0, duration: 0.5 })
.to('.panel-3', { opacity: 1, duration: 1 });
What the demo shows
The demo is a four-section scroll story. Section 1: a grid of cards fades in with stagger as you reach them. Section 2: a heading drifts in parallax as you scroll past. Section 3: a pinned storytelling section where three panels swap in and out as you scrub. Section 4: a number counts from 0% to 100% as you scroll through it. A sticky bar at the top shows overall page scroll progress.
ScrollTrigger는 스크롤 이벤트를 GSAP 애니메이션 타임라인에 연결합니다. 핵심 패턴은 세 가지입니다: 진입 시 재생, 스크럽(스크롤에 연동), 핀.
진입 시 재생
각 .card가 뷰포트의 80%에 진입할 때 fade up 애니메이션.
start: 'top 80%', toggleActions: 'play none none reverse'.
스크럽 효과
.hero-text가 스크롤에 따라 왼쪽으로 parallax 이동.
scrollTrigger: { scrub: 1 }.
scrub 값이 클수록 부드럽고 지연됨.
핀 섹션
.panel이 뷰포트에 고정되는 동안 3개의 카드가 순차적으로 fade in.
scrollTrigger: { pin: true, end: '+=300%', scrub: true }.
디버그 마커
현재 ScrollTrigger가 예상대로 동작하지 않아.
markers: true 추가해서 trigger/start/end 위치 시각화해줘.
markers: true는 배포 전 반드시 제거해야 하는 디버그 도구입니다.
gsap.registerPlugin(ScrollTrigger);
// ── Page scroll progress bar ──────────────────────────────────
const progressBar = document.getElementById('progressBar');
const progressLabel = document.getElementById('progressLabel');
ScrollTrigger.create({
trigger: document.body,
start: 'top top',
end: 'bottom bottom',
onUpdate: self => {
const pct = Math.round(self.progress * 100);
progressBar.style.width = pct + '%';
progressLabel.textContent = pct + '%';
},
});
// ── Hero entrance ─────────────────────────────────────────────
const heroTl = gsap.timeline({ defaults: { ease: 'power3.out' } });
heroTl
.from('.site-nav', { y: -16, opacity: 0, duration: 0.4 })
.from('.eyebrow', { opacity: 0, duration: 0.3 }, '-=0.1')
.from('.hero h1', { y: 32, opacity: 0, duration: 0.6 }, '-=0.15')
.from('.sub', { y: 16, opacity: 0, duration: 0.45 }, '-=0.3');
// ── Pattern 01: Stagger fade-in cards ────────────────────────
// When the card grid scrolls into view at 75%, cards appear one by one
gsap.from('.scroll-card', {
scrollTrigger: {
trigger: '.card-grid',
start: 'top 75%',
},
y: 32,
opacity: 0,
duration: 0.55,
stagger: 0.1,
ease: 'power2.out',
});
// Section heading also fades in
gsap.from('.cards-section .section-label, .cards-section h2, .section-sub', {
scrollTrigger: {
trigger: '.cards-section',
start: 'top 80%',
},
y: 20,
opacity: 0,
duration: 0.5,
stagger: 0.1,
ease: 'power2.out',
});
// ── Pattern 02: Parallax with scrub ──────────────────────────
// Heading drifts upward at a slower rate than scroll speed
gsap.to('.parallax-heading', {
scrollTrigger: {
trigger: '#parallaxSection',
start: 'top bottom',
end: 'bottom top',
scrub: 1,
},
y: -80,
ease: 'none',
});
// Parallax section label and sub fade in normally
gsap.from('#parallaxSection .section-label, .parallax-sub', {
scrollTrigger: {
trigger: '#parallaxSection',
start: 'top 75%',
},
opacity: 0,
y: 16,
duration: 0.6,
stagger: 0.15,
ease: 'power2.out',
});
// ── Pattern 03: Pinned storytelling ──────────────────────────
// Section pins in place; 3 panels swap as you scroll through
const storyTl = gsap.timeline({
scrollTrigger: {
trigger: '#storySection',
start: 'top top',
end: '+=300%',
pin: true,
scrub: 1,
anticipatePin: 1,
},
});
// Panel 1 is already visible (opacity: 1 in CSS)
// Fade out panel 1, fade in panel 2
storyTl
.to('#panel1', { opacity: 0, y: -20, duration: 0.4 }, 0.4)
.fromTo('#panel2', { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.4 }, 0.6)
.to('#panel2', { opacity: 0, y: -20, duration: 0.4 }, 1.4)
.fromTo('#panel3', { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.4 }, 1.6);
// ── Pattern 04: onUpdate progress counter ────────────────────
// A number counts from 0 to 100 driven by scroll progress
const counterNumber = document.getElementById('counterNumber');
const counterFill = document.getElementById('counterFill');
const counterNote = document.querySelector('.counter-note');
ScrollTrigger.create({
trigger: '#counterSection',
start: 'top 80%',
end: 'bottom 20%',
onUpdate: self => {
const pct = Math.round(self.progress * 100);
counterNumber.textContent = pct + '%';
counterFill.style.width = pct + '%';
},
onEnter: () => {
counterNote.textContent = 'Counting...';
},
onLeave: () => {
counterNote.textContent = 'Done ✓';
},
onLeaveBack: () => {
counterNote.textContent = 'Keep scrolling ↓';
},
});
// Counter section label + heading entrance
gsap.from('.counter-section .section-label, .counter-section h2, .counter-section .section-sub', {
scrollTrigger: {
trigger: '#counterSection',
start: 'top 80%',
},
y: 20,
opacity: 0,
duration: 0.5,
stagger: 0.1,
ease: 'power2.out',
});
// Footer fade in
gsap.from('.footer-pad', {
scrollTrigger: {
trigger: '.footer-pad',
start: 'top 90%',
},
opacity: 0,
y: 16,
duration: 0.5,
ease: 'power2.out',
});