Backgrounds
Flow Scroll
#scroll#gallery#cards
implementedInfinityUI component
This component is fully implemented in InfinityUI and wired into the docs and registry flow.
Installation
Use the registry command to add the component source, and install any package dependencies if needed.
Infinity Registry
Install the component source into your project with the shadcn CLI.
npx shadcn@latest add https://infinityui-pearl.vercel.app/r/flow-scrollPackage Dependencies
Install the npm packages used by this component source.
npm install framer-motionComponent Code
Copy and paste this code into your component file.
tsx
"use client";
import { useRef } from "react";
import Image from "next/image";
import { motion, type MotionValue, useScroll, useTransform } from "framer-motion";
function FlowScrollCard({
image,
index,
scrollYProgress,
totalItems,
}: {
image: string;
index: number;
scrollYProgress: MotionValue<number>;
totalItems: number;
}) {
const itemsPerRow = 3;
const prev = Math.max(0, index - itemsPerRow);
const next = Math.min(totalItems - 1, index + itemsPerRow);
const previousRow = Math.floor(prev / itemsPerRow);
const currentRow = Math.floor(index / itemsPerRow);
const nextRow = Math.floor(next / itemsPerRow);
const totalRows = Math.max(1, Math.ceil(totalItems / itemsPerRow));
const scrollRangePerRow = 1 / totalRows;
const entryAnimation = previousRow / totalRows - scrollRangePerRow;
const currentPosition = currentRow / totalRows;
const exitAnimation = nextRow / totalRows + scrollRangePerRow * 2;
const offsetToAdd = (scrollRangePerRow / Math.max(totalItems, 1)) * (currentRow + 2);
const range = [
0,
entryAnimation - offsetToAdd,
currentPosition - offsetToAdd,
currentPosition - offsetToAdd,
exitAnimation - offsetToAdd,
1,
];
const scale = useTransform(scrollYProgress, range, [0.5, 0.5, 1, 1, 0.5, 0.5]);
const isLeft = index % itemsPerRow === 0;
const isRight = index % itemsPerRow === 2;
const xTransform = useTransform(scrollYProgress, range, [
isLeft ? "100%" : isRight ? "-100%" : "0%",
isLeft ? "100%" : isRight ? "-100%" : "0%",
"0%",
"0%",
"0%",
"0%",
]);
const rotate = useTransform(scrollYProgress, range, [
isLeft ? -20 : isRight ? 20 : 0,
isLeft ? -20 : isRight ? 20 : 0,
0,
0,
0,
0,
]);
const shadowY = useTransform(scrollYProgress, range, [50, 50, 25, 25, -50, -50]);
return (
<motion.div
style={{
scale,
x: xTransform,
rotate,
zIndex: !isLeft && !isRight ? 1 : 0,
boxShadow: useTransform(
shadowY,
(value) => `0px ${value}px 40px 10px rgba(0, 0, 0, 0.1)`
),
}}
className="h-32 w-full overflow-hidden rounded-2xl sm:max-w-48 sm:h-60 md:max-w-60 md:h-72"
>
<Image
src={image}
alt={image}
width={1000}
height={1000}
className="w-full h-full object-cover"
/>
</motion.div>
);
}
export interface FlowScrollProps {
images: string[];
}
export function FlowScroll({ images }: FlowScrollProps) {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
container: ref,
offset: ["start start", "end end"],
});
return (
<div
ref={ref}
className="flex justify-center w-full h-full overflow-y-auto py-36 pb-96"
>
<div className="grid h-max grid-cols-3 gap-4 md:gap-6 lg:gap-12">
{images.map((image, index) => (
<FlowScrollCard
key={`flow-scroll-card-${index}`}
image={image}
index={index}
scrollYProgress={scrollYProgress}
totalItems={images.length}
/>
))}
</div>
</div>
);
}
export default FlowScroll;