RiaChoi Text Logo.
Cinematic Flip Card Interface with React & Framer Motion

FramerMotion

React

Cinematic Flip Card Interface with React & Framer Motion

Let's build a cinematic 3D flip card interface using React, Vite, Tailwind CSS and Framer Motion.

Ria ChoiMay 25th, 2026

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.

mtl Coffee Shop
mtl Coffee Shop
mtl Coffee Shop
mtl Coffee Shop

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

  1. Stystem Architecture
    1. Folder Structure
    2. Framer Motion
  2. Step1. Setting Up
  3. Step2. Engineering
  4. Step3. Putting Things Together
  5. 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.md

Framer 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.

 

  1. The blinds needed to flip sequentially, one after another, almost like a real rolling display.
  2. 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.
  3. 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.

plan
Plan

So, I came up with these ideas.

  • CardStack handles the bigger sequence logic
    Controls the overall animation flow, sequence timing, image order, and synchronization between all blinds.
  • FlipCard handles 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.
Detailed Planning
Detailed Planning

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 | Image2

To 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>
    );
  1. motion.div creates the animated card container
    Framer Motion replaces a normal div so the card can animate smoothly.
  2. onMouseEnter and onMouseLeave handle hover interaction
    These trigger the flip animation when the user hovers over the blind.
  3. animate controls the rotation state
    rotateY flips the card horizontally in 3D space.
  4. transition controls animation timing
    Sets how long the flip takes and makes the motion feel smoother with easeInOut.
  5. transformStyle: "preserve-3d" enables true 3D flipping
    Without this, the card would look flat during rotation.
  6. backfaceVisibility: "hidden" hides the backside during flips
    Prevents mirrored or glitchy rendering while rotating.
  7. CardFace renders 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. :)

The Presentation
The Presentation

 

You Can Checkout the Banner Page Below (Desktop Only)

https://mtl-banner.vercel.app/

Github

https://github.com/riiach/mtl-banner

 

 

Share

Related contents