Scroll Stack Cards
It is a refined, minimal UI component built for Next.js and Framer Motion.It uses lenis for smooth scroll behaviour.
Fight Club
The first rule of Fight Club is: You do not talk about Fight Club. The second rule of Fight Club is: You DO NOT talk about Fight Club.

The Boys
When you’re famous, people let you get away with murder. Homelander doesn’t need permission — he needs applause.

The Dark Knight
Why so serious? You either die a hero, or you live long enough to see yourself become the villain.

Inception
You mustn’t be afraid to dream a little bigger, darling. An idea is like a virus — resilient, highly contagious.

Interstellar
We used to look up at the sky and wonder at our place in the stars. Now we just look down and worry about our place in the dirt.

Dependencies
npm install
framer-motionlenisCode
"use client";
import React, { useEffect, useRef } from "react";
import { motion, MotionValue, useScroll, useTransform } from "framer-motion";
import Lenis from "lenis";
import { cn } from "@/lib/utils";
export const projects = [
{
title: "Fight Club",
description:
"The first rule of Fight Club is: You do not talk about Fight Club. The second rule of Fight Club is: You DO NOT talk about Fight Club.",
src: "/fght-club.jpg",
link: "fght-club.jpg",
color: "#BBACAF",
},
{
title: "The Boys",
description:
"When you’re famous, people let you get away with murder. Homelander doesn’t need permission — he needs applause.",
src: "https://res.cloudinary.com/dmqwpwo6c/image/upload/v1776147234/the-boys_rsaqok.jpg",
link: "the.jpg",
color: "#977F6D",
},
{
title: "The Dark Knight",
description:
"Why so serious? You either die a hero, or you live long enough to see yourself become the villain.",
src: "https://res.cloudinary.com/dmqwpwo6c/image/upload/v1776147231/dark-knight_e1hv4i.jpg",
link: "dark-knight",
color: "#C2491D",
},
{
title: "Inception",
description:
"You mustn’t be afraid to dream a little bigger, darling. An idea is like a virus — resilient, highly contagious.",
src: "https://res.cloudinary.com/dmqwpwo6c/image/upload/v1776147231/inception_hutvvu.jpg",
link: "https://res.cloudinary.com/dmqwpwo6c/image/upload/v1776147233/15_uojijm.jpg",
color: "#862429",
},
{
title: "Interstellar",
description:
"We used to look up at the sky and wonder at our place in the stars. Now we just look down and worry about our place in the dirt.",
src: "/interstellar.jpg",
link: "https://res.cloudinary.com/dmqwpwo6c/image/upload/v1776147231/16_jeq4en.jpg",
color: "#88A2BD",
},
];
function ScrollCards() {
const containerRef = useRef(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start start", "end end"],
});
// remove comment if want lenis
// useEffect(() => {
// const lenis = new Lenis();
// function raf(time: any) {
// lenis.raf(time);
// requestAnimationFrame(raf);
// }
// requestAnimationFrame(raf);
// }, []);
return (
<motion.div
ref={containerRef}
className="flex justify-center flex-col items-center my-[10vh]"
>
{projects.map((items: any, idx) => {
const targetScale = 1 - (projects.length - idx) * 0.05;
return (
<Cards
title={items.title}
description={items.description}
src={items.src}
link={items.link}
color={items.color}
key={idx}
index={idx}
progress={scrollYProgress}
range={[idx * 0.25, 1]}
targetScale={targetScale}
/>
);
})}
</motion.div>
);
}
export default ScrollCards;
type cardProps = {
title?: string;
description?: string;
src?: string;
link?: string;
color?: string;
className?: string;
index: number;
progress: MotionValue<number>;
targetScale: number;
range: number[];
};
function Cards({
className,
title,
description,
src,
link,
color,
index,
range,
targetScale,
progress,
}: cardProps) {
const containerRef = useRef(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "start start"],
});
const scrollVal = useTransform(scrollYProgress, [0, 1], [2, 1]);
const scale = useTransform(progress, range, [1, targetScale]);
return (
<div
ref={containerRef}
className="font-bebas max-w-[60vw] h-screen flex items-center justify-center sticky top-[10vh]"
>
<motion.div
style={{
scale: scale,
backgroundColor: color,
top: `calc(-10% + ${index * 25}px)`,
}}
className={cn(
className,
"relative w-[50vw] h-[60vh] rounded-2xl flex justify-between items-center gap-2 p-20",
)}
>
<div className="w-full h-full flex flex-col gap-5 justify-center items-center">
<h2 className="text-5xl font-bold text-center">{title}</h2>
<p className="text-xl">{description}</p>
</div>
<div className="max-h-[300px] w-full flex justify-center rounded-2xl items-center overflow-hidden">
<motion.div
style={{ scale: scrollVal }}
className="absolute top-0 -right-5 h-[300px] w-[200px] rounded-2xl overflow-hidden"
>
{/* <MaskCard overlayBg={link}/> */}
<img
src={src}
alt=""
className="object-cover w-full h-full rounded-2xl overflow-hidden"
/>
</motion.div>
</div>
</motion.div>
</div>
);
}