Skip to main content

Command Palette

Search for a command to run...

Creating Smooth Slide Animations with React Motion: A Beginner's Guide

Published
8 min read

Introduction

Have you ever wondered how to create those smooth, professional-looking slide animations you see on modern websites? In this tutorial, we'll build an automated image carousel with beautiful vertical slide transitions using Framer Motion and React.

By the end of this guide, you'll understand:

  • How to set up Framer Motion in your React project

  • How to create smooth slide transitions

  • How to manage animation states

  • How to build both automatic and manual slide navigation

Let's dive in! 🚀


What We're Building

We'll create a slide carousel component with these features:

  • Automatic sliding: Slides change every 8 seconds

  • Smooth vertical transitions: Slides move up and down elegantly

  • Manual navigation: Click tabs to jump to any slide

  • Bi-directional animations: Different animations for moving forward vs backward


Prerequisites

Before we start, make sure you have:

  • Basic knowledge of React (hooks like useState and useEffect)

  • A React project set up (Next.js, Create React App, or Vite)

  • Node.js and npm/pnpm/yarn installed


Step 1: Install Framer Motion

First, let's install Framer Motion in your project:

# Using npm
npm install motion

# Using pnpm
pnpm add motion

# Using yarn
yarn add motion

Note: We're using the new motion package (formerly framer-motion)


Step 2: Understanding the Core Concepts

Before we code, let's understand the key concepts:

Animation States

We'll track three states:

  1. Current Slide: The slide currently visible

  2. Previous Slide: The slide we're transitioning from

  3. Animation Direction: Whether we're moving to the next or previous slide

Motion Variants

Variants are predefined animation states in Framer Motion. They make it easy to create reusable animations:

const slideVariants = {
  nextSlide: { translateY: 800 },   // Move down
  prevSlide: { translateY: -800 }   // Move up
}

Step 3: Setting Up the Component Structure

Let's start by creating our component skeleton:

"use client"; // If using Next.js App Router

import { motion, Variants } from "motion/react";
import { useEffect, useState } from "react";

type PanelSlideProps = {
  slideImgs: string[];      // Array of image URLs
  uiToolbar?: string;       // Optional toolbar overlay
};

function PanelSlide({ slideImgs, uiToolbar }: PanelSlideProps) {
  // State management
  const [currentSlide, setCurrentSlide] = useState(0);
  const [previousSlide, setPreviousSlide] = useState<null | number>(null);
  const [animationState, setAnimationState] = useState<"nextSlide" | "prevSlid" | "">("");

  return (
    <div className="w-full h-full flex flex-col items-center">
      {/* Slide container will go here */}
    </div>
  );
}

export default PanelSlide;

What's happening here?

  • currentSlide: Tracks which slide is currently showing (0-indexed)

  • previousSlide: Tracks the slide we're animating away from

  • animationState: Determines the direction of animation


Step 4: Creating Animation Variants

Now let's define our animation variants. These control how slides move in and out:

function PanelSlide({ slideImgs, uiToolbar }: PanelSlideProps) {
  const [currentSlide, setCurrentSlide] = useState(0);
  const [previousSlide, setPreviousSlide] = useState<null | number>(null);
  const [animationState, setAnimationState] = useState<"nextSlide" | "prevSlid" | "">("");

  // Animation for the CURRENT slide (the one entering)
  const currentSlideAnimation: Variants = {
    nextSlide: {
      translateY: 800,  // Start below viewport
    },
    prevSlid: {
      translateY: -800, // Start above viewport
    },
  };

  // Animation for the PREVIOUS slide (the one exiting)
  const previousSlideAnimation: Variants = {
    nextSlide: {
      translateY: -800, // Exit upward
    },
    prevSlid: {
      translateY: 800,  // Exit downward
    },
  };

  // ... rest of component
}

Key Insight:

  • When going forward (nextSlide): New slide comes from below (800px), old slide exits upward (-800px)

  • When going backward (prevSlid): New slide comes from above (-800px), old slide exits downward (800px)


Step 5: Implementing Auto-Play with useEffect

Let's add automatic slide advancement every 8 seconds:

useEffect(() => {
  const interval = setInterval(() => {
    if (currentSlide < slideImgs.length - 1) {
      // Not at the last slide, go forward
      setPreviousSlide(currentSlide);
      setCurrentSlide(currentSlide + 1);
      setAnimationState("nextSlide");
    } else {
      // At the last slide, loop back to first
      setPreviousSlide(currentSlide);
      setCurrentSlide(0);
      setAnimationState("prevSlid");
    }
  }, 8000); // 8 seconds

  // Cleanup: Clear interval when component unmounts
  return () => clearInterval(interval);
}, [currentSlide, slideImgs.length]);

Breaking it down:

  1. setInterval runs every 8 seconds

  2. We check if there's a next slide

  3. Update states: previous → current, current → next

  4. Set animation direction

  5. Clean up the interval to prevent memory leaks


Step 6: Building the Slide Container

Now for the exciting part - rendering the animated slides!

return (
  <div className="w-full max-w-[1192px] h-[728px] flex flex-col items-center">
    {/* Main slide container */}
    <div className="w-full h-full bg-gray-100 relative rounded-xl border-4 overflow-hidden">

      {/* Slides container */}
      <div className="w-full h-full absolute inset-0 z-0">

        {/* CURRENT SLIDE */}
        {currentSlide !== null && currentSlide !== undefined && (
          <div className="w-full h-full absolute px-12 flex items-center justify-center">
            <motion.img
              initial={previousSlide === null ? false : animationState}
              animate={previousSlide === null ? false : { translateY: 0 }}
              transition={{ duration: 0.35, ease: "easeInOut", delay: 0.3 }}
              variants={currentSlideAnimation}
              src={slideImgs[currentSlide]}
              alt={`slide-${currentSlide + 1}`}
              key={`current-slide-${currentSlide + 1}`}
              className="w-full rounded-lg"
            />
          </div>
        )}

        {/* PREVIOUS SLIDE */}
        {previousSlide !== null && previousSlide !== undefined && (
          <div className="w-full h-full absolute px-12 flex items-center justify-center">
            <motion.img
              initial={previousSlide === null ? false : { translateY: 0 }}
              animate={previousSlide === null ? false : animationState}
              transition={{ duration: 0.35, ease: "easeInOut", delay: 0.3 }}
              variants={previousSlideAnimation}
              src={slideImgs[previousSlide]}
              alt={`slide-${previousSlide + 1}`}
              key={`previous-slide-${previousSlide + 1}`}
              className="w-full rounded-lg"
            />
          </div>
        )}
      </div>

      {/* Optional toolbar overlay */}
      {uiToolbar && (
        <div className="relative inset-0 z-30 pointer-events-none">
          <img src={uiToolbar} alt="ui-toolbar" className="w-full h-full object-cover" />
        </div>
      )}
    </div>
  </div>
);

Understanding the Motion Props:

  • initial: Starting position of the animation

    • false = No initial animation (for first load)

    • animationState = Use the variant key ("nextSlide" or "prevSlid")

  • animate: Target position to animate to

    • { translateY: 0 } = Center position (visible)
  • variants: Object containing animation definitions

  • transition: Animation timing

    • duration: 0.35 seconds

    • ease: "easeInOut" for smooth acceleration/deceleration

    • delay: 0.3 seconds (allows previous slide to start exiting first)

  • key: React key for proper re-rendering


Step 7: Adding Navigation Tabs

Let's add clickable tabs for manual navigation:

type SlideTabsProps = {
  slideImgs: string[];
  currentSlide: number;
  setAnimationState: React.Dispatch<React.SetStateAction<"nextSlide" | "prevSlid" | "">>;
  setPreviousSlide: React.Dispatch<React.SetStateAction<null | number>>;
  setCurrentSlide: React.Dispatch<React.SetStateAction<number>>;
};

function SlideTabs({
  slideImgs,
  currentSlide,
  setAnimationState,
  setPreviousSlide,
  setCurrentSlide,
}: SlideTabsProps) {
  return (
    <div className="w-full h-20 my-5 relative overflow-hidden">
      <div className="py-4 flex gap-6 items-center justify-center">
        {slideImgs.map((_, index) => (
          <div
            key={`slide-tab-${index + 1}`}
            className={`
              cursor-pointer py-1.5 px-4 rounded-lg transition-all duration-300
              hover:text-white
              ${index === currentSlide ? 'text-white bg-blue-500' : 'text-gray-400'}
            `}
            onClick={() => {
              if (index === currentSlide) return; // Already on this slide

              // Determine animation direction
              if (index > currentSlide) {
                setAnimationState("nextSlide"); // Moving forward
              } else {
                setAnimationState("prevSlid");  // Moving backward
              }

              // Update slides
              setPreviousSlide(currentSlide);
              setCurrentSlide(index);
            }}
          >
            Slide {index + 1}
          </div>
        ))}
      </div>
    </div>
  );
}

Logic explained:

  1. Check if clicked tab is already active

  2. Determine direction: forward if index > currentSlide, backward otherwise

  3. Set the previous slide for animation

  4. Update to the new slide


Step 8: Putting It All Together

Here's the complete component:

"use client";

import { motion, Variants } from "motion/react";
import { useEffect, useState } from "react";

type PanelSlideProps = {
  slideImgs: string[];
  uiToolbar?: string;
};

type SlideTabsProps = {
  slideImgs: string[];
  currentSlide: number;
  setAnimationState: React.Dispatch<React.SetStateAction<"nextSlide" | "prevSlid" | "">>;
  setPreviousSlide: React.Dispatch<React.SetStateAction<null | number>>;
  setCurrentSlide: React.Dispatch<React.SetStateAction<number>>;
};

function PanelSlide({ slideImgs, uiToolbar }: PanelSlideProps) {
  const [currentSlide, setCurrentSlide] = useState(0);
  const [previousSlide, setPreviousSlide] = useState<null | number>(null);
  const [animationState, setAnimationState] = useState<"nextSlide" | "prevSlid" | "">("");

  // Animation variants
  const currentSlideAnimation: Variants = {
    nextSlide: { translateY: 800 },
    prevSlid: { translateY: -800 },
  };

  const previousSlideAnimation: Variants = {
    nextSlide: { translateY: -800 },
    prevSlid: { translateY: 800 },
  };

  // Auto-play effect
  useEffect(() => {
    const interval = setInterval(() => {
      if (currentSlide < slideImgs.length - 1) {
        setPreviousSlide(currentSlide);
        setCurrentSlide(currentSlide + 1);
        setAnimationState("nextSlide");
      } else {
        setPreviousSlide(currentSlide);
        setCurrentSlide(0);
        setAnimationState("prevSlid");
      }
    }, 8000);

    return () => clearInterval(interval);
  }, [currentSlide, slideImgs.length]);

  return (
    <div className="w-full max-w-[1192px] h-full flex flex-col items-center">
      <div className="w-full h-[728px] bg-gray-100 relative rounded-xl border-4 overflow-hidden">
        <div className="w-full h-full absolute inset-0 z-0">
          {/* Current Slide */}
          {currentSlide !== null && currentSlide !== undefined && (
            <div className="w-full h-full absolute px-12 flex items-center justify-center">
              <motion.img
                initial={previousSlide === null ? false : animationState}
                animate={previousSlide === null ? false : { translateY: 0 }}
                transition={{ duration: 0.35, ease: "easeInOut", delay: 0.3 }}
                variants={currentSlideAnimation}
                src={slideImgs[currentSlide]}
                alt={`slide-${currentSlide + 1}`}
                key={`current-slide-${currentSlide + 1}`}
                className="w-full rounded-lg"
              />
            </div>
          )}

          {/* Previous Slide */}
          {previousSlide !== null && previousSlide !== undefined && (
            <div className="w-full h-full absolute px-12 flex items-center justify-center">
              <motion.img
                initial={{ translateY: 0 }}
                animate={animationState}
                transition={{ duration: 0.35, ease: "easeInOut", delay: 0.3 }}
                variants={previousSlideAnimation}
                src={slideImgs[previousSlide]}
                alt={`slide-${previousSlide + 1}`}
                key={`previous-slide-${previousSlide + 1}`}
                className="w-full rounded-lg"
              />
            </div>
          )}
        </div>

        {/* Optional UI Toolbar Overlay */}
        {uiToolbar && (
          <div className="relative inset-0 z-30 pointer-events-none">
            <img src={uiToolbar} alt="ui-toolbar" className="w-full h-full object-cover" />
          </div>
        )}
      </div>

      {/* Navigation Tabs */}
      <SlideTabs
        slideImgs={slideImgs}
        currentSlide={currentSlide}
        setAnimationState={setAnimationState}
        setPreviousSlide={setPreviousSlide}
        setCurrentSlide={setCurrentSlide}
      />
    </div>
  );
}

function SlideTabs({
  slideImgs,
  currentSlide,
  setAnimationState,
  setPreviousSlide,
  setCurrentSlide,
}: SlideTabsProps) {
  return (
    <div className="w-full h-20 my-5 relative overflow-hidden">
      <div className="py-4 flex gap-6 items-center justify-center">
        {slideImgs.map((_, index) => (
          <div
            key={`slide-tab-${index + 1}`}
            className={`
              cursor-pointer py-1.5 px-4 rounded-lg transition-all duration-300
              hover:text-white whitespace-nowrap
              ${index === currentSlide ? 'text-white bg-blue-500' : 'text-gray-400'}
            `}
            onClick={() => {
              if (index === currentSlide) return;

              if (index > currentSlide) {
                setAnimationState("nextSlide");
              } else {
                setAnimationState("prevSlid");
              }

              setPreviousSlide(currentSlide);
              setCurrentSlide(index);
            }}
          >
            Slide {index + 1}
          </div>
        ))}
      </div>
    </div>
  );
}

export default PanelSlide;