Card Stack Animations on Scroll with Framer Motion and Tailwind CSS
Status: #seedling
Collapse cards on scroll
This is the classic animation that a Google search for "card stacking animation on scroll" would provide you, and it looks like this:
Source: https://blog.olivierlarose.com/tutorials/cards-parallax
The cited tutorial is a great resources, implementing the useScroll
and useTransform
hooks from Framer to create this effect.
Reveal cards on scroll
For the desired animation I had to implement at work, the outcome was the one from above - except reversed. You start from the stacked cards, and by scrolling down, the cards stacked below are revealed one by one on scroll:
Similar to the scrolling animation from before, we employ the framer
hooks to control the rising, scale and opacity values for the 2nd and 3rd card. The first card does not need to have any transformations, just the z-index
CSS value needs to be bigger than the one of the other cards.
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ['0 0.1', 'end end'],
});
const card2Y = useTransform(scrollYProgress, [0, 0.35], ['3%', '108%']);
const card2Scale = useTransform(scrollYProgress, [0.1, 0.4, 0.5], [0.92, 0.92, 1]);
const card3Y = useTransform(scrollYProgress, [0, 0.4, 0.92], ['100%', '70%', '215%']);
const card3Scale = useTransform(scrollYProgress, [0.4, 0.9, 1], [0.92, 0.92, 1]);
const card2Opacity = useTransform(scrollYProgress, [0, 0.5, 0.55], [0, 0, 1]);
I had to test out multiple value mappings based on the card heights and the scrolling container. It was tricky to create a decent scrolling reveal, and still it's not the smoothest one.
In my situation, at least it was convenient that the heights of the container and cards were fixed:
<div
className="flex justify-center bg-black relative min-h-[1880px]"
ref={containerRef}
>
<div className="relative flex flex-col gap-8">
<div className="z-[3] origin-bottom">
<div className="h-[500px] bg-white rounded-2xl transition-all duration-300" />
</div>
<motion.div
className="absolute top-0 right-0 left-0 z-[2] origin-bottom"
style={{
y: card2Y,
scale: card2Scale,
}}
>
<div className="h-[500px] bg-primary rounded-2xl transition-all duration-300" />
</motion.div>
<motion.div
className="absolute top-0 right-0 left-0 z-[1] origin-bottom"
style={{
y: card3Y,
scale: card3Scale,
opacity: card3Opacity,
}}
>
<div className="h-[500px] bg-white rounded-2xl transition-all duration-300" />
</motion.div>
</div>
</div>