Debugging Animations with Claude Code
Animation bugs are uniquely frustrating because they’re invisible — nothing moves, or it moves wrong, and there’s no error in the console. Knowing the five most common GSAP bugs and how to describe them saves hours.
The debugging prompt pattern
Always describe what you see (or don’t see), then paste the code:
이 코드에서 왜 [symptom]이 발생하는지 설명하고 고쳐줘. 변경한 내용만 설명해줘.
[paste your code]
Claude Code will identify the root cause, show the fix, and explain only what changed.
Bug 1 — autoAlpha vs opacity
If you set opacity: 0 in CSS and then gsap.to('.el', { opacity: 1 }), it often works. But if GSAP’s own from set it to 0 first, the element might stay hidden after the animation ends due to inline style conflicts.
Fix: Use autoAlpha instead of opacity when you control visibility with GSAP. autoAlpha sets both opacity and visibility, and GSAP manages the lifecycle cleanly.
// Buggy
gsap.from('.box', { opacity: 0 });
// Fixed
gsap.from('.box', { autoAlpha: 0 });
Bug 2 — ScrollTrigger not firing
Two common causes: the trigger element isn’t in the DOM yet, or start is set so late the trigger never enters the viewport.
Fix: Set start: 'top 80%' so the animation fires before the element fully enters the viewport. Add markers: true during development to see the trigger lines on screen.
scrollTrigger: {
trigger: '.section',
start: 'top 80%',
markers: true, // ← remove before production
}
Bug 3 — from vs to confusion
gsap.to('.box', { x: 0 }) does nothing if the box is already at x:0. This is the most common “why isn’t this animating?” bug.
Fix: Use gsap.from to animate from a starting value to the current position.
// Buggy — animates TO x:0, but element is already there
gsap.to('.box', { x: 0, opacity: 1 });
// Fixed — animates FROM x:-40 TO current position
gsap.from('.box', { x: -40, opacity: 0 });
Bug 4 — DOM timing
document.querySelectorAll('.item') called before the DOM is ready returns an empty NodeList. GSAP silently animates nothing.
Fix: Wrap your GSAP code in a DOMContentLoaded listener, or place your <script> tag at the end of <body>.
// Buggy — runs before .item elements exist
gsap.from(document.querySelectorAll('.item'), { y: 20 });
// Fixed
window.addEventListener('DOMContentLoaded', () => {
gsap.from('.item', { y: 20, stagger: 0.1 });
});
Bug 5 — Timeline position collisions
If you use '-=2' on a tween that only lasts 0.5s, you push it before the start of the timeline (negative time). GSAP clips it, the tween never plays, and the sequence looks broken.
Fix: Keep overlap amounts smaller than the previous tween’s duration.
// Buggy — '-=2' exceeds the 0.4s previous tween
tl.from('.a', { opacity: 0, duration: 0.4 })
.from('.b', { opacity: 0, duration: 0.6 }, '-=2');
// Fixed
tl.from('.a', { opacity: 0, duration: 0.4 })
.from('.b', { opacity: 0, duration: 0.6 }, '-=0.2');
What the demo shows
The demo is an interactive debug playground with five panels. Each panel has a “Buggy” button and a “Fixed” button that run the broken and corrected animations on a visible target element. A before/after code diff shows exactly what changed for each bug.
GSAP 애니메이션이 예상대로 동작하지 않을 때, 문제를 Claude Code에 정확히 설명하면 빠르게 디버그할 수 있습니다.
일반적인 문제와 프롬프트
애니메이션이 실행되지 않을 때:
gsap.to('.box', { x: 200, duration: 1 }) 이 실행되지 않아.
콘솔 에러 없음. ScrollTrigger 없이 기본 트윈인데 안 돼.
ScrollTrigger가 틀린 위치에서 발동할 때:
이 ScrollTrigger가 너무 이르게 발동돼.
markers: true로 확인해줘. start 값을 어떻게 조정해야 해?
transform 충돌:
GSAP가 x를 애니메이션하는데 CSS transform: rotate(45deg)가 리셋돼.
두 transform을 함께 유지하는 방법은?
디버그 체크리스트
GSAP 문제 발생 시 순서대로 확인:
- ScrollTrigger 플러그인 등록됐는가? (
gsap.registerPlugin(ScrollTrigger)) - 타겟 요소가 DOM에 존재하는가?
- CSS
will-change나transform충돌 없는가? markers: true로 trigger 위치 시각화
에러를 Claude Code에 설명할 때는 기대하는 동작과 실제 동작을 모두 설명하세요.
gsap.registerPlugin(ScrollTrigger);
// ── Nav entrance ──────────────────────────────────────────────
gsap.from('.site-nav', { y: -16, opacity: 0, duration: 0.4, ease: 'power3.out' });
// ── Hero entrance ─────────────────────────────────────────────
gsap.from(['.eyebrow', '.hero h1', '.sub'], {
y: 20,
opacity: 0,
duration: 0.55,
stagger: 0.1,
ease: 'power2.out',
});
// ── Bug section headings fade in on scroll ────────────────────
gsap.utils.toArray('.bug-section').forEach(section => {
gsap.from(section.querySelector('.bug-header'), {
scrollTrigger: { trigger: section, start: 'top 85%' },
y: 16,
opacity: 0,
duration: 0.5,
ease: 'power2.out',
});
});
// ─────────────────────────────────────────────────────────────
// BUG 1: autoAlpha vs opacity
// ─────────────────────────────────────────────────────────────
// The box has style="opacity:0" in HTML.
// Buggy: gsap.from with opacity leaves inline style; element stays invisible.
// Fixed: gsap.from with autoAlpha manages visibility lifecycle.
document.querySelector('[data-bug="1"][data-mode="buggy"]').addEventListener('click', () => {
// Reset
gsap.set('#box1', { clearProps: 'all' });
document.getElementById('box1').style.opacity = '0';
// Buggy: animate opacity, but inline style wins in some browsers/states
gsap.from('#box1', {
opacity: 0,
y: 20,
duration: 0.5,
ease: 'power2.out',
// This appears to work at first, but the inline style="" can conflict.
// To make the bug visible we simulate it: after animation, re-apply opacity:0
onComplete: () => {
// Simulate: inline opacity:0 wasn't cleared by GSAP, element snaps back
document.getElementById('box1').style.opacity = '0';
}
});
});
document.querySelector('[data-bug="1"][data-mode="fixed"]').addEventListener('click', () => {
// Clear everything so autoAlpha starts fresh
gsap.set('#box1', { clearProps: 'all' });
// Fixed: autoAlpha handles opacity AND visibility. No inline style conflict.
gsap.from('#box1', {
autoAlpha: 0,
y: 20,
duration: 0.5,
ease: 'power2.out',
// Element stays visible after animation completes
});
});
// ─────────────────────────────────────────────────────────────
// BUG 2: ScrollTrigger start value
// ─────────────────────────────────────────────────────────────
// Buggy: start: 'top top' — fires only when element top hits viewport top.
// Fixed: start: 'top 80%' — fires when element is 20% into viewport.
let st2buggy = null;
let st2fixed = null;
document.querySelector('[data-bug="2"][data-mode="buggy"]').addEventListener('click', () => {
if (st2buggy) st2buggy.kill();
if (st2fixed) st2fixed.kill();
gsap.set('#box2', { clearProps: 'all', x: -60, opacity: 0 });
updateTriggerInfo('triggerInfo2', false, 'start: top top (likely won\'t fire)');
st2buggy = ScrollTrigger.create({
trigger: '#box2',
start: 'top top', // ← usually never fires for mid-page elements
onEnter: () => {
gsap.to('#box2', { x: 0, opacity: 1, duration: 0.6 });
updateTriggerInfo('triggerInfo2', true, 'Fired! (rare)');
},
});
ScrollTrigger.refresh();
});
document.querySelector('[data-bug="2"][data-mode="fixed"]').addEventListener('click', () => {
if (st2buggy) st2buggy.kill();
if (st2fixed) st2fixed.kill();
gsap.set('#box2', { clearProps: 'all', x: -60, opacity: 0 });
updateTriggerInfo('triggerInfo2', false, 'Waiting for scroll...');
st2fixed = ScrollTrigger.create({
trigger: '#box2',
start: 'top 80%', // ← fires when element enters viewport
onEnter: () => {
gsap.to('#box2', { x: 0, opacity: 1, duration: 0.6 });
updateTriggerInfo('triggerInfo2', true, 'Fired! ✓');
},
});
ScrollTrigger.refresh();
// Scroll element into view to demo it
document.getElementById('box2').scrollIntoView({ behavior: 'smooth', block: 'center' });
});
function updateTriggerInfo(id, active, text) {
const el = document.getElementById(id);
if (!el) return;
el.querySelector('.ti-dot').classList.toggle('active', active);
el.querySelector('.ti-text').textContent = text;
}
// ─────────────────────────────────────────────────────────────
// BUG 3: from vs to
// ─────────────────────────────────────────────────────────────
// Buggy: gsap.to('#box3', { x: 0 }) — box is already at x:0. Nothing moves.
// Fixed: gsap.from('#box3', { x: -60 }) — box slides in from left.
document.querySelector('[data-bug="3"][data-mode="buggy"]').addEventListener('click', () => {
gsap.set('#box3', { clearProps: 'all' });
const box = document.getElementById('box3');
box.textContent = 'Nothing moves';
// Buggy: to x:0 from x:0 — no movement
gsap.to('#box3', {
x: 0,
opacity: 1,
duration: 0.6,
ease: 'power2.out',
onComplete: () => { box.textContent = '... still nothing'; }
});
});
document.querySelector('[data-bug="3"][data-mode="fixed"]').addEventListener('click', () => {
gsap.set('#box3', { clearProps: 'all' });
const box = document.getElementById('box3');
box.textContent = 'Slides in ✓';
// Fixed: from x:-60, animates into natural position
gsap.from('#box3', {
x: -60,
opacity: 0,
duration: 0.6,
ease: 'power2.out',
});
});
// ─────────────────────────────────────────────────────────────
// BUG 4: DOM timing
// ─────────────────────────────────────────────────────────────
// Buggy: querySelectorAll called before items ready (we simulate the empty list).
// Fixed: query inside DOMContentLoaded callback (or after DOM ready).
document.querySelector('[data-bug="4"][data-mode="buggy"]').addEventListener('click', () => {
// Simulate the bug: pass an empty NodeList to GSAP
gsap.set('.list-item', { clearProps: 'all', opacity: 0 });
const emptyList = document.querySelectorAll('.list-item-DOESNT-EXIST');
// emptyList.length === 0 → GSAP animates nothing
gsap.from(emptyList, {
y: 20,
opacity: 0,
stagger: 0.1,
duration: 0.5,
// Nothing happens — items stay hidden
onComplete: () => {
// Reveal them after a pause to show they were never animated
setTimeout(() => {
gsap.set('.list-item', { opacity: 0.25 });
}, 800);
}
});
});
document.querySelector('[data-bug="4"][data-mode="fixed"]').addEventListener('click', () => {
gsap.set('.list-item', { clearProps: 'all' });
// Fixed: DOM is already ready by the time this click fires
// In real code you'd wrap in DOMContentLoaded
gsap.from('.list-item', {
y: 20,
opacity: 0,
stagger: 0.1,
duration: 0.5,
ease: 'power2.out',
});
});
// ─────────────────────────────────────────────────────────────
// BUG 5: Timeline position collision
// ─────────────────────────────────────────────────────────────
// Buggy: '-=2' overlap on a 0.4s tween → pushes B to negative time.
// Fixed: '-=0.2' overlap — safe, B plays just before A ends.
document.querySelector('[data-bug="5"][data-mode="buggy"]').addEventListener('click', () => {
gsap.set(['#tl5a', '#tl5b'], { clearProps: 'all' });
const tl = gsap.timeline();
tl.from('#tl5a', { opacity: 0, y: 16, duration: 0.4 })
// '-=2' exceeds 0.4s duration → B pushed to t < 0 → clipped → B appears to snap
.from('#tl5b', { opacity: 0, y: 16, duration: 0.6 }, '-=2');
});
document.querySelector('[data-bug="5"][data-mode="fixed"]').addEventListener('click', () => {
gsap.set(['#tl5a', '#tl5b'], { clearProps: 'all' });
const tl = gsap.timeline();
tl.from('#tl5a', { opacity: 0, y: 16, duration: 0.4 })
// '-=0.2' is safe: B starts 0.2s before A ends, smooth overlap
.from('#tl5b', { opacity: 0, y: 16, duration: 0.6 }, '-=0.2');
});