Why is the 4th image showing first in my GSAP scroll-trigger animation?

I’m working on a scroll-triggered animation using GSAP in a React app. The goal is to animate a sequence of photos as the user scrolls, with each photo becoming visible one after another. However, when the page loads, the last photo (4th image) shows up first and then animates in the correct order from 1 to 4. When I try to fix the issue by moving the first photo to the front, it results in a duplicate first photo before the animation starts, which then animates correctly.
const photosRef = useRef<HTMLDivElement>(null);
const photos = [
"/images/slide1.jpg",
"/images/slide2.jpg",
"/images/slide3.jpg",
"/images/slide4.jpg",
];

useLayoutEffect(() => {
const ctx = gsap.context(() => {
const timeline = gsap.timeline({
scrollTrigger: {
trigger: photosRef.current, // Target the section
start: "top top", // Starts when the top of the section hits the top of the viewport
end: `+=${photos.length * window.innerHeight}`, // Adjust the scroll duration to fit the number of slides
scrub: true,
pin: true,
},
});

const photoElements = gsap.utils.toArray(".photo") as HTMLElement[];

// Animate in order (from slide1 to slide4)
photoElements.forEach((photo, index) => {
gsap.set(photo, { zIndex: 0, scale: 1 }); // Example scale effect

timeline.to(
photo,
{
zIndex: 1, // Bring photo to the front
scale: 1, // Scale photo to normal size
opacity: 1, // Fade photo in
duration: 1,
ease: "power2.in",
},
index * 0.5 // Add delay based on index for sequential animation
);
});
}, photosRef);

return () => ctx.revert();
}, []);
const photosRef = useRef<HTMLDivElement>(null);
const photos = [
"/images/slide1.jpg",
"/images/slide2.jpg",
"/images/slide3.jpg",
"/images/slide4.jpg",
];

useLayoutEffect(() => {
const ctx = gsap.context(() => {
const timeline = gsap.timeline({
scrollTrigger: {
trigger: photosRef.current, // Target the section
start: "top top", // Starts when the top of the section hits the top of the viewport
end: `+=${photos.length * window.innerHeight}`, // Adjust the scroll duration to fit the number of slides
scrub: true,
pin: true,
},
});

const photoElements = gsap.utils.toArray(".photo") as HTMLElement[];

// Animate in order (from slide1 to slide4)
photoElements.forEach((photo, index) => {
gsap.set(photo, { zIndex: 0, scale: 1 }); // Example scale effect

timeline.to(
photo,
{
zIndex: 1, // Bring photo to the front
scale: 1, // Scale photo to normal size
opacity: 1, // Fade photo in
duration: 1,
ease: "power2.in",
},
index * 0.5 // Add delay based on index for sequential animation
);
});
}, photosRef);

return () => ctx.revert();
}, []);
4 Replies
~MARSMAN~
~MARSMAN~2w ago
how does your photosRef look like? is it a container with 4 img tags? and do they have position absolute? my guess is that the last image is taking its z-index based on its order in the DOM, which is the heighest of its siblings. and then once your parent componenet ( the one with the GSAP scrolTrigger ) hits the top of the viewport, your images gets their stacking as you want them.
roycwilliams
roycwilliamsOP2w ago
Hey, you’re correct. My photoRefs contains 4 images in an absolute position. When I place the opacity to 0 it works fine but my goal is to have users see the photo before triggering the animation just so they won’t think it’s just white space. Any thoughts on how I can achieve this?
~MARSMAN~
~MARSMAN~2w ago
One way to do it is to give the first img a zindex of 5. This way it will stay on top of the last image whether the parent is in view or not. And its zindex will change while scrolling the parent.
roycwilliams
roycwilliamsOP2w ago
My man thank you!

Did you find this page helpful?