Intro
I was sitting in a cafe one afternoon when I completely fell in love with their banner display.
It used rolling blinds that flipped over to reveal different images, almost like a slow moving film strip.
I really wish I had taken a video of it because the motion was what made it so beautiful, but all I have are a few photos.


I decided to recreate the exact same banner, and at first I thought, “This is going to be a piece of cake.” (And I was in the middle of a huge project, so this was supposed to be a getaway for me), and it absolutely was not.
I had to rethink the logic over and over again just to make the panels flip naturally and respond smoothly to hover interationcs. What looked simple from the outside became one of those projects that quietly tests your patience as a developer.
But after a lot of experimenting, tweaking, and rebuilding, I finally got it working the way I imagined. And today, I’m going to show you exactly how I built it.
Index
- Stystem Architecture
- Folder Structure
- Framer Motion
- Step1. Setting Up
- Step2. Engineering
- Step3. Putting Things Together
- Final Product
System Architecture
I’m not going to dive too deeply into the S/A here just because this is such a tiny single page project, but I do want to talk a little about the folder structure and why I chose Framer Motion instead of building everything with pure CSS.
Folder Structure
I used React, Tailwind CSS, and Framer Motion.
Incredibly simple stack.
mtl-banner/
├── public/
│
├── src/
│ ├── components/
│ │ ├── cards/
│ │ │ ├── CardFace.jsx
│ │ │ ├── CardStack.jsx
│ │ │ ├── FlipCard.jsx
│ │ │ └── cards.data.js
│ │ │
│ │ └── layout/
│ │ ├── Container.jsx
│ │ └── Mobile.jsx
│ │
│ ├── App.jsx
│ ├── globals.css
│ └── main.jsx
│
├── package.json
├── vite.config.js
└── README.mdFramer Motion
Framer Motion is basically an animation library for React that makes movement feel effortless.
Instead of manually handling complicated CSS keyframes or animation timing, you can describe animations in a much more natural way directly inside your components. Things like hover effects, smooth transitions, page movement, and flipping interactions become surprisingly simple to build.
For this project, it was perfect because the entire banner depends on movement feeling smooth and responsive. The cards needed to rotate, stack, and react to hover interactions without feeling stiff, and Framer Motion handled that beautifully.
Framer Motion
https://www.framer.com/dictionary/framer-motion
Step1. Setting Up
I’m going to skip the setup process since I already shared the folder structure and dependencies you’ll need.
Instead, let’s take a quick look at what each file actually does before we jump into the fun part.
Just a little peek behind the curtain before the real magic starts. 🥳
cards/
CardFace.jsx
This component is responsible for rendering a single side of the blind.
Think of it as one image layer. It handles the visual appearance of the card face.
FlipCard.jsx
This is where the actual flip animation logic lives.
It controls rotation, hover interaction, animation timing etc…
CardStack.jsx
This component creates the full rolling blind effect by stacking multiple flip cards together. Instead of animation one large image, it splits the banner into several smaller sections and controls how they move together as a group.
cards.data.js
A simple data file that stores the images and content used by cards.
layout/
Container.jsx
This acts as the main layout wrapper for the page.
It handles spacing, alignment, screen sizing, and keeps the banner centered properly on the page.
cards.data.js
↓
CardFace.jsx // Card image and positioning
↓
FlipCard.jsx // Flipping Logic, Hover Logic
↓
CardStack.jsx // Rendering FlipCards, Another flipping logic, Setting delay
↓
Container.jsx // Container for everything
Then, you prepare assets that look asthetically pleasing to you.
Then you code the container. It’s like setting up a box for everything.
Step2. Engineering
Now comes the engineering part. The moment where you stop thinking about how cool the animation looks and start asking yourself, “Alright… how am I actually going to make this work?”
There are honestly a lot of ways you could approach this, and I went through more trial and error than I expected before everything finally clicked together. But throughout all of that experimentation, the core goal never changed.
- The blinds needed to flip sequentially, one after another, almost like a real rolling display.
- They also needed to respond to interaction naturally. Hovering over them had to trigger smooth mouse enter and mouse leave animations without breaking the flow of the sequence.
- And probably the trickiest part of all, after one full flip sequence finished, every card had to reveal the next intended image correctly regardless of its position in the stack.
That sounds simple at first, but once multiple cards are animating independently while also sharing synchronized image states, things get complicated very quickly.

So, I came up with these ideas.
CardStackhandles the bigger sequence logic
Controls the overall animation flow, sequence timing, image order, and synchronization between all blinds.FlipCardhandles the smaller interaction logic
Responsible for the actual card rotation, hover interaction, mouse enter and leave actions, and transition animation.- Separate sequence logic from interaction logic
Keeping them independent made the system much easier to debug and scale later. - Separate image data from card data
Images and card configuration are stored independently to keep the components cleaner and more reusable.

You’ll probably understand the structure much better once you see the actual data setup behind it.
cards.data.js
export const images = [
"/CardFace1.png",
"/CardFace2.jpg",
"/CardFace3.png",
];
export const cards = Array.from({ length: 60 }, (_, index) => ({
id: index,
index,
}));This looks incredibly simple, but this separation is what makes the entire system work.
At first, I thought the image data and card logic should stay together because technically the image lives on the card. But that approach quickly became messy once interactions started happening independently.
The problem is that cards can be hovered individually while the full banner sequence is also progressing at the same time. That means one card could already be on a different flip state from the rest of the sequence.
Image1 | Image1 | Image2(User flipped) | Image1
>>>>>> Next Sequence
Image2 | Image2 | Image3 | Image2To avoid everything falling out of sync, I separated the image rendering logic from the card flip logic and simply kept their indexes aligned. That way, the animation state and the image state can move independently without breaking the sequence.
The Flipping Logic
CardStack
const CardStack = () => {
const [activeCardIndex, setActiveCardIndex] = useState(-1);
const [targetFaceIndex, setTargetFaceIndex] = useState(0);
useEffect(() => {
const delay = activeCardIndex === -1 ? SET_DELAY : FLIP_SPEED;
const timer = setTimeout(() => {
setActiveCardIndex((prev) => {
if (prev === -1) return 0;
if (prev >= cards.length - 1) return -1;
return prev + 1;
});
}, delay);
return () => clearTimeout(timer);
}, [activeCardIndex]);
=> This logic controls the timing of the entire flip sequence.activeCardIndex keeps track of which blind is currently flipping, and once the sequence reaches the last card, it resets back to -1 so the system can pause briefly before starting the next full animation cycle again.
FlipCard
const FlipCard = ({
index,
totalCards,
images = [],
isActive,
targetFaceIndex,
setTargetFaceIndex,
}) => {=> First, the FlipCard components receive targetFaceIndex along with all the sequence related states from CardStack.
// Automatic sequential flip
useEffect(() => {
if (!isActive) return;
if (!images.length) return;
// First card defines target state
if (index === 0) {
const nextFace =
(faceIndex + 1) % images.length;
setTargetFaceIndex(nextFace);
flipToFace(nextFace, "forward");
return;
}
// All other cards follow first card state
flipToFace(targetFaceIndex, "forward");
=> This logic handles the sequential flip behavior, and honestly, this is the most important part of the entire application. Every other card follows the state of the very first card.
What makes this so important is that the cards are not simply flipping based on their own previous state like a typical toggle animation. Instead, every card has to stay synchronized with the first card so that no matter what side it was previously showing, it always renders the same image state as the main sequence.
// Hover reverse flip
const handleHoverStart = () => {
if (hasHovered) return;
if (!images.length) return;
setHasHovered(true);
const prevFace =
(faceIndex - 1 + images.length)
% images.length;
flipToFace(prevFace, "backward");
};
const handleHoverEnd = () => {
setHasHovered(false);
};
if (!images.length) return null;
=> This is the hover flip logic. You’ll understand why I stored the flip state separately once you see how Framer Motion handles animations.
Step3. Framer Motion
Framer Motion makes animation so much easier to work with. Instead of fighting CSS keyframes and transition timing, you can control everything directly through state and props in React, which feels much more natural when building interactive UI.
It’s honestly one of the most useful animation libraries out there for frontend developers. I’d probably put it alongside tools like GSAP, Three.js, and React Three Fiber when it comes to creating rich interactive experiences on the web.
And this is how I used it on my project.
return (
<motion.div
className="flex-1 overflow-hidden"
onMouseEnter={handleHoverStart}
onMouseLeave={handleHoverEnd}
animate={{
rotateY: `${rotation}deg`,
}}
transition={{
duration: FLIP_DURATION / 1000,
ease: "easeInOut",
}}
style={{
transformStyle: "preserve-3d",
backfaceVisibility: "hidden",
}}
>
<CardFace
image={images[faceIndex]}
index={index}
totalCards={totalCards}
/>
</motion.div>
);motion.divcreates the animated card container
Framer Motion replaces a normaldivso the card can animate smoothly.onMouseEnterandonMouseLeavehandle hover interaction
These trigger the flip animation when the user hovers over the blind.animatecontrols the rotation staterotateYflips the card horizontally in 3D space.transitioncontrols animation timing
Sets how long the flip takes and makes the motion feel smoother witheaseInOut.transformStyle: "preserve-3d"enables true 3D flipping
Without this, the card would look flat during rotation.backfaceVisibility: "hidden"hides the backside during flips
Prevents mirrored or glitchy rendering while rotating.CardFacerenders the actual image slice
Each card displays one section of the full banner image.
Final Product
And here it is. Honestly, it’s beautiful to look at.
There’s something really satisfying about watching the blinds continuously flip and cycle through the images. It almost feels more like a moving art piece than a UI component. You could genuinely leave this running on an iPad sitting on your desk for hours and just glance over at it throughout the day. :)

You Can Checkout the Banner Page Below (Desktop Only)
https://mtl-banner.vercel.app/
Github
https://github.com/riiach/mtl-banner
