Overlays
Flip Scroll
#scroll#flip#3d
?
?
?
?
?
?
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/flip-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 BackFace() {
return (
<div
className="absolute top-0 left-0 flex items-center justify-center w-full h-[500px] rounded-3xl border border-white/15 bg-black text-7xl font-bold text-white"
style={{
transform: "rotateY(180deg) translateZ(-1px)",
backfaceVisibility: "hidden",
}}
>
?
</div>
);
}
function FlipScrollItem({
image,
index,
isLeft,
scrollYProgress,
totalItems,
mode,
}: {
image: string;
index: number;
isLeft: boolean;
scrollYProgress: MotionValue<number>;
totalItems: number;
mode: "alternate" | "normal";
}) {
const direction = isLeft ? -1 : 1;
const position = index / totalItems;
const translateX = useTransform(
scrollYProgress,
[0, position, 1],
[-index * 400 * direction, 0, (totalItems - index) * 400 * direction]
);
const normalRotateY = useTransform(
scrollYProgress,
[0, position - 0.04, position - 0.015, position + 0.015, position + 0.04, 1],
[180, 180, 0, 0, -180, -180]
);
const alternateRotateY = useTransform(
scrollYProgress,
[0, position, 1],
[index * -180 * direction, 0, (totalItems - index) * 180 * direction]
);
return (
<motion.div
style={{
translateX,
perspective: 1000,
transformStyle: "preserve-3d",
}}
className="absolute h-[500px] max-w-sm w-full overflow-visible rounded-3xl"
>
<motion.div
style={{
rotateY: mode === "normal" ? normalRotateY : alternateRotateY,
transformStyle: "preserve-3d",
}}
className="relative"
>
<Image
src={image}
alt={image}
width={1000}
height={1000}
className="absolute top-0 left-0 h-[500px] w-full rounded-3xl object-cover"
style={{
backfaceVisibility: "hidden",
transform: "translateZ(1px)",
}}
/>
<BackFace />
</motion.div>
</motion.div>
);
}
export interface FlipScrollProps {
items: { image: string }[];
mode?: "alternate" | "normal";
}
export function FlipScroll({
items,
mode = "normal",
}: FlipScrollProps) {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
container: ref,
offset: ["start start", "end end"],
});
return (
<div
ref={ref}
className="relative w-full h-full overflow-y-auto"
style={{ minHeight: "400px" }}
>
<div className="absolute top-0 left-0 w-full h-full">
<div className="w-full" style={{ height: Math.max((items.length - 3) * 500, 500) }} />
</div>
<div className="sticky top-0 left-0 grid w-full h-full grid-cols-1">
<div className="relative flex flex-col items-center justify-center w-full h-full">
{items.map((item, index) => (
<FlipScrollItem
key={`flip-scroll-item-${index}`}
image={item.image}
index={index}
isLeft
scrollYProgress={scrollYProgress}
totalItems={items.length}
mode={mode}
/>
))}
</div>
</div>
</div>
);
}
export default FlipScroll;