alright here’s the thing — your ScrollTrigger isn’t broken, it’s just not dying when you leave the page.
on local dev everything constantly reloads so triggers get reset.
in production? nope. they survive. like cockroaches.
so the first time you load / it works.
you navigate away, come back, the old trigger is still pinned somewhere in gsap limbo, new trigger tries to run → boom, nothing happens.
kill the old trigger before making a new one
kill timeline + trigger properly on cleanup (return)
stop trusting revertOnUpdate to magically fix it — it won't
optionally turn off pinReparent, it causes visual chaos inside react
once you reset gsap manually, the animation works again every time you come back to home page — production included.
useGSAP(() => {
if (pathname !== '/') return;
const el = sectionRef.current;
if (!el) return;
ScrollTrigger.getById('process-section-pin')?.kill(); // kill ghosts
const q = gsap.utils.selector(el);
const isMobile = window.innerWidth < 768;
const scrollDistance = isMobile ? 1500 : 2000;
const tl = gsap.timeline()
.to({}, { duration: 0.4 })
.to(q('.slide-0'), { top: '100%', duration: 0.25, ease: 'power2.inOut' })
.to(q('.slide-1'), { top: '0%', duration: 0.25, ease: 'power2.inOut' }, '<')
.to({}, { duration: 0.4 })
.to(q('.slide-1'), { top: '100%', duration: 0.25, ease: 'power2.inOut' })
.to(q('.slide-2'), { top: '0%', duration: 0.25, ease: 'power2.inOut' }, '<')
.to({}, { duration: 0.4 });
const trigger = ScrollTrigger.create({
id: 'process-section-pin',
trigger: el,
start: 'top top',
end: `+=${scrollDistance}`,
scrub: 0.5,
pin: true,
pinSpacing: true,
animation: tl,
invalidateOnRefresh: true,
});
const onResize = () => trigger.refresh();
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
trigger.kill();
tl.kill();
};
}, { dependencies:[pathname], scope:sectionRef });