Hello,
I created an infinite vertical scroll like effect for my cards, it took a while but with the helps of forums and some demos, it worked perfectly for me on local.
However, when I check production, I see that it's not working properly, cards are appearing small and origin seems like below, some cards get bigger some are not, and most ambiguous one is if i scroll up, animation works properly for like 4-5 cards, but when i scroll down card disappears immediately.
I deploy with vercel and there is no club plugin as you can see.
Can you please enlighten me about the issue? I suspected with some css conflict but couldn't find a way to solve it. It took my whole day and decided to ask some help here.
Purpose of animation ( in case there is a much more basic or consistent way to do this )
-Avoid page scroll over container and avoid animation work when page is scrolled outside card container.
-Give a seamless vertical scroll effect without showing any scroll bar.
Stack:
- Next14, Typescript, Vercel for deployment
Thank you for your help.
local.mov
```
import React, { useEffect } from "react";
import { gsap } from "gsap";
import { IWorkOption, workOptions } from "../../utilities";
import styles from "./card-container.module.css";
const scrollSpeedMultiplier = 0.0001;
const CardContainer = ({ setItemSelected }: { setItemSelected: React.Dispatch<React.SetStateAction<IWorkOption>> }) => {
useEffect(() => {
const cards = Array.from(document.querySelectorAll(".single-one-pair"));
const spacing = 0.1;
const seamlessLoop = buildSeamlessLoop(cards, spacing, animateCard);
const playhead = { offset: 0 };
const scrub = gsap.to(playhead, {
offset: 0,
onUpdate: () => {
seamlessLoop.time(wrapTime(-playhead.offset));
},
duration: 0.5,
ease: "power3",
paused: true,
});
function animateCard(element: Element) {
const tl = gsap.timeline();
tl.fromTo(element, { scaleX: 0.8, scaleY: 0.1 }, { scaleX: 1, scaleY: 0.9, opacity: 1, duration: 0.5, yoyo: true, repeat: 1, ease: "power1.in", immediateRender: false }).fromTo(
element,
{ yPercent: -400 },
{ yPercent: 400, duration: 1, ease: "none", immediateRender: false },
0
);
return tl;
}
function buildSeamlessLoop(items: Element[], spacing: number, animateFunc: (element: Element) => gsap.core.Timeline) {
let rawSequence = gsap.timeline({ paused: true }),
seamlessLoop = gsap.timeline({ paused: true });
items
.concat(items)
.concat(items)
.concat(items)
.forEach((item, i) => {
let anim = animateFunc(items[i % items.length]);
rawSequence.add(anim, i * spacing);
});
seamlessLoop.fromTo(rawSequence, { time: spacing * items.length }, { time: `+=${spacing * items.length}`, duration: spacing * items.length, ease: "none" });
return seamlessLoop;
}
function wrapTime(offset: number) {
return gsap.utils.wrap(0, seamlessLoop.duration())(offset);
}
const ulElement = document.querySelector(".pair-container");
function handleMouseEnter() {
ulElement?.addEventListener("wheel", handleWheel as EventListener);
ulElement?.addEventListener("touchstart", handleTouchStart as EventListener);
ulElement?.addEventListener("touchmove", handleTouchMove as EventListener);
ulElement?.addEventListener("touchend", handleTouchEnd as EventListener);
scrub.play(); // Start the animation when the mouse enters
}
function handleMouseLeave() {
ulElement?.removeEventListener("wheel", handleWheel as EventListener);
ulElement?.removeEventListener("touchstart", handleTouchStart as EventListener);
ulElement?.removeEventListener("touchmove", handleTouchMove as EventListener);
ulElement?.removeEventListener("touchend", handleTouchEnd as EventListener);
scrub.pause(); // Pause the animation when the mouse leaves
}
function handleWheel(event: WheelEvent) {
event.stopPropagation();
event.preventDefault();
console.log("Wheel event deltaY:", event.deltaY); // Log deltaY for debugging
scrub.vars.offset += event.deltaY * scrollSpeedMultiplier; // Adjust scroll speed as necessary
scrub.invalidate().restart(false);
}
let touchStartY = 0;
function handleTouchStart(event: TouchEvent) {
touchStartY = event.touches[0].clientY;
}
function handleTouchMove(event: TouchEvent) {
const touchEndY = event.touches[0].clientY;
const deltaY = touchStartY - touchEndY;
touchStartY = touchEndY; // Update the start position for the next move
console.log("Touch move deltaY:", deltaY); // Log deltaY for debugging
scrub.vars.offset += deltaY * scrollSpeedMultiplier; // Adjust scroll speed as necessary
scrub.invalidate().restart();
event.preventDefault();
}
function handleTouchEnd(event: TouchEvent) {
touchStartY = 0; // Reset the touch start position
}
ulElement?.addEventListener("mouseenter", handleMouseEnter);
ulElement?.addEventListener("mouseleave", handleMouseLeave);
return () => {
ulElement?.removeEventListener("mouseenter", handleMouseEnter);
ulElement?.removeEventListener("mouseleave", handleMouseLeave);
ulElement?.removeEventListener("wheel", handleWheel as EventListener);
ulElement?.removeEventListener("touchstart", handleTouchStart as EventListener);
ulElement?.removeEventListener("touchmove", handleTouchMove as EventListener);
ulElement?.removeEventListener("touchend", handleTouchEnd as EventListener);
gsap.killTweensOf(playhead);
};
}, []);
return (
<div className={styles.body}>
<ul className="pair-container" style={{ width: "100%", height: "100%", display: "flex", alignItems: "center" }}>
{workOptions.map((card, i) => (
<li
key={i}
className="single-one-pair"
style={{ listStyle: "none", padding: 0, margin: 0, width: "100%", height: "80%", position: "absolute", display: "flex", justifyContent: "center", alignItems: "center" }}
>
<div key={i} className={styles.pairItem} style={{ backgroundColor: "red" }} onClick={() => setItemSelected(card)}>
<div className={styles.overlay}>
<h2 className={styles.overlayText}>{i}</h2>
</div>
</div>
</li>
))}
</ul>
</div>
);
};
export default CardContainer;
```
prod.mov