Parallax Cards
Parallax scrolling effect using overlapping cards.
"use client";
import { useEffect, useRef } from "react";
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
import { useScroll, cancelFrame, frame } from "motion/react";
import { ReactLenis } from "lenis/react";
import type { LenisRef } from "lenis/react";
import { Button } from "@/components/ui/button";
import ParallaxCardEffect from "@/components/core/parallax-card-effect";
import { cn } from "@/lib/utils";
const cardItems = [
{
title: "Urban Reflections",
description:
"An exploration of how cities shape our perception of light, shadow, and movement. Each frame reflects the rhythm of everyday urban life.",
src: "https://plus.unsplash.com/premium_vector-1744029045529-3fcd4f715be6?q=80&w=2748&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
className: "bg-red-200"
},
{
title: "Wilderness Silence",
description:
"A visual journey into untouched landscapes where silence becomes the most powerful storyteller.",
src: "https://plus.unsplash.com/premium_vector-1697729849330-ef5db47d3246?q=80&w=2814&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
className: "bg-green-200"
},
{
title: "Ocean Whispers",
description:
"Capturing the ever-changing moods of the sea, from calm horizons to raging storms that leave a timeless imprint.",
src: "https://plus.unsplash.com/premium_vector-1697729780111-058eea198643?q=80&w=2648&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
className: "bg-orange-200"
},
{
title: "Desert Dreams",
description:
"Through vast emptiness and shifting sands, this project highlights the fragile beauty of desert environments.",
src: "https://plus.unsplash.com/premium_vector-1721220820381-71da4f8b1adf?q=80&w=2360&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
className: "bg-indigo-200"
},
{
title: "Mountain Echoes",
description:
"The monumental presence of mountains and their interplay with light and atmosphere form the essence of this series.",
src: "https://plus.unsplash.com/premium_vector-1725703994559-09c72f2a317d?q=80&w=2360&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
className: "bg-purple-200"
}
];
export type CardItemType = (typeof cardItems)[number];
export default function Page() {
const lenisRef = useRef<LenisRef>(null);
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start start", "end end"]
});
useEffect(() => {
function update(data: { timestamp: number }) {
const time = data.timestamp;
lenisRef.current?.lenis?.raf(time);
}
frame.update(update, true);
return () => cancelFrame(update);
}, []);
const ParallaxCardItem = ({ item, id }: { item: CardItemType; id: number }) => {
const targetScale = 1 - (cardItems.length - id) * 0.05;
return (
<ParallaxCardEffect
id={id}
progress={scrollYProgress}
range={[id * 0.25, 1]}
targetScale={targetScale}
className={cn("relative flex flex-col rounded-lg px-14 py-8", item.className)}>
<div className="space-y-4 text-center">
<h2 className="text-center text-2xl font-semibold">{item.title}</h2>
<p>{item.description}</p>
<Button>
See more <ChevronRightIcon />
</Button>
</div>
</ParallaxCardEffect>
);
};
return (
<>
<ReactLenis root options={{ autoRaf: false }} ref={lenisRef} />
<div ref={containerRef}>
<div className="flex h-screen items-center justify-center gap-2 text-xl">
Scroll <ChevronDownIcon />
</div>
<div className="mx-auto max-w-2xl pt-14">
{cardItems.map((cardItem, i) => (
<ParallaxCardItem item={cardItem} key={i} id={i} />
))}
</div>
</div>
</>
);
}
Installation
Install the following dependencies:
npm install motion clsx tailwind-merge
Copy and paste the following code into your project:
"use client";
import React from "react";
import { cn } from "@/lib/utils";
import { motion, useTransform } from "motion/react";
interface CardProps {
id: number;
className?: string;
progress: any;
range: number[];
targetScale: number;
children?: React.ReactNode;
}
export default function ParallaxCardEffect({
id,
className,
progress,
range,
targetScale,
children
}: CardProps) {
const scale = useTransform(progress, range, [1, targetScale]);
return (
<div className="sticky top-0 flex h-screen items-center justify-center">
<motion.div
style={{
scale,
top: `calc(-5vh + ${id * 30}px)`
}}
className={className}>
{children}
</motion.div>
</div>
);
}
Update the import paths to match your project setup.
Usage
"use client";
import { useEffect, useRef } from "react";
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
import { useScroll, cancelFrame, frame } from "motion/react";
import { ReactLenis } from "lenis/react";
import type { LenisRef } from "lenis/react";
import { Button } from "@/components/ui/button";
import ParallaxCardEffect from "@/components/core/parallax-card-effect";
import { cn } from "@/lib/utils";
const cardItems = [
{
title: "Urban Reflections",
description:
"An exploration of how cities shape our perception of light, shadow, and movement. Each frame reflects the rhythm of everyday urban life.",
src: "https://plus.unsplash.com/premium_vector-1744029045529-3fcd4f715be6?q=80&w=2748&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
className: "bg-red-200"
},
{
title: "Wilderness Silence",
description:
"A visual journey into untouched landscapes where silence becomes the most powerful storyteller.",
src: "https://plus.unsplash.com/premium_vector-1697729849330-ef5db47d3246?q=80&w=2814&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
className: "bg-green-200"
},
{
title: "Ocean Whispers",
description:
"Capturing the ever-changing moods of the sea, from calm horizons to raging storms that leave a timeless imprint.",
src: "https://plus.unsplash.com/premium_vector-1697729780111-058eea198643?q=80&w=2648&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
className: "bg-orange-200"
},
{
title: "Desert Dreams",
description:
"Through vast emptiness and shifting sands, this project highlights the fragile beauty of desert environments.",
src: "https://plus.unsplash.com/premium_vector-1721220820381-71da4f8b1adf?q=80&w=2360&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
className: "bg-indigo-200"
},
{
title: "Mountain Echoes",
description:
"The monumental presence of mountains and their interplay with light and atmosphere form the essence of this series.",
src: "https://plus.unsplash.com/premium_vector-1725703994559-09c72f2a317d?q=80&w=2360&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
className: "bg-purple-200"
}
];
export type CardItemType = (typeof cardItems)[number];
export default function Page() {
const lenisRef = useRef<LenisRef>(null);
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start start", "end end"]
});
useEffect(() => {
function update(data: { timestamp: number }) {
const time = data.timestamp;
lenisRef.current?.lenis?.raf(time);
}
frame.update(update, true);
return () => cancelFrame(update);
}, []);
const ParallaxCardItem = ({ item, id }: { item: CardItemType; id: number }) => {
const targetScale = 1 - (cardItems.length - id) * 0.05;
return (
<ParallaxCardEffect
id={id}
progress={scrollYProgress}
range={[id * 0.25, 1]}
targetScale={targetScale}
className={cn("relative flex flex-col rounded-lg px-14 py-8", item.className)}>
<div className="space-y-4 text-center">
<h2 className="text-center text-2xl font-semibold">{item.title}</h2>
<p>{item.description}</p>
<Button>
See more <ChevronRightIcon />
</Button>
</div>
</ParallaxCardEffect>
);
};
return (
<>
<ReactLenis root options={{ autoRaf: false }} ref={lenisRef} />
<div ref={containerRef}>
<div className="flex h-screen items-center justify-center gap-2 text-xl">
Scroll <ChevronDownIcon />
</div>
<div className="mx-auto max-w-2xl pt-14">
{cardItems.map((cardItem, i) => (
<ParallaxCardItem item={cardItem} key={i} id={i} />
))}
</div>
</div>
</>
);
}
Props
Prop | Type | Default |
---|---|---|
children? | ReactNode | undefined |
targetScale? | number | - |
range? | number[] | - |
progress? | MotionValue<number> | - |
className? | string | undefined |
id? | number | - |
Marquee Effect
Add dynamic movement to your UI with Marquee effect, ideal for highlighting important information with smooth, continuous scrolling. Built with Tailwind CSS and Motion.
Ripple Effect
Ripple effect is a modern animation component designed to enrich user interaction by creating a dynamic ripple effect when clicking on buttons or any HTML element. Built with Tailwind CSS and Motion.