Creating Smooth Slide Animations with React Motion: A Beginner's Guide
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
useStateanduseEffect)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
motionpackage (formerlyframer-motion)
Step 2: Understanding the Core Concepts
Before we code, let's understand the key concepts:
Animation States
We'll track three states:
Current Slide: The slide currently visible
Previous Slide: The slide we're transitioning from
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 fromanimationState: 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:
setIntervalruns every 8 secondsWe check if there's a next slide
Update states: previous → current, current → next
Set animation direction
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 animationfalse= 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 definitionstransition: Animation timingduration: 0.35 secondsease: "easeInOut" for smooth acceleration/decelerationdelay: 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:
Check if clicked tab is already active
Determine direction: forward if
index > currentSlide, backward otherwiseSet the previous slide for animation
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;