Back to Blog
How to Build a Pro React Animated Carousel (Free Source Code) – cover image comparing React animation libraries

How to Build a Pro React Animated Carousel (Free Source Code)

·
Karan

Building a custom React animated carousel that feels natural, works flawlessly on mobile devices, and handles complex gestures can be surprisingly difficult. We've all seen rigid, awkward sliders that jump instantly between images without any smooth transitions. In modern frontend development, especially if you are running a SaaS company or a web agency, having fluid micro-interactions isn’t just a nice-to-have visual treat—it’s a massive trust signal for your users. Quality UI translates to perceived product quality, which ultimately drives higher conversions and better user retention.

If you have tried building one before using standard react state, you know the pain: managing the index limits, handling drag-and-drop boundary physics, setting up an auto-play timer that respects user interactions, and ensuring your animations fire exactly as intended.

In this comprehensive tutorial, we are going to walk through the exact process of creating a buttery-smooth slider from scratch. By combining the declarative power of modern front-end practices with the physics-based springs of Framer Motion, we can achieve incredible results.


Key Takeaways

  • Building a premium-feeling React animated carousel requires managing direction-based state and utilizing AnimatePresence.
  • Standard CSS transitions often fall short when dealing with dynamic swipe gestures and physics-based spring animations.
  • Ensuring web accessibility (ARIA attributes, keyboard focus management) is critical for any slider component moving into a production environment.
  • If you don't have the time to debug custom physics code, dropping a pre-built component into your app is often the smartest move for busy SaaS founders.

Table of Contents

  1. The UX Problem with Standard Sliders
  2. Prerequisites: Our Tech Stack
  3. Step 1: Setting Up the Component State
  4. Step 2: Adding Framer Motion for Smooth Transitions
  5. Step 3: Handling Autoplay and Pause on Hover
  6. Step 4: Accessibility Best Practices
  7. The Final Polish: Combining Everything
  8. The Full Code
  9. The Faster Alternative for Busy Founders: ogBlocks
  10. Frequently Asked Questions (FAQ)

The UX Problem with Standard Sliders

A poorly built React animated carousel that locks up on mobile or jumps abruptly between slides signals a lack of polish to your users. Substituting standard CSS transitions with genuine spring physics builds immediate trust. Let's build a buttery-smooth, fluid slider from scratch.

Prerequisites: Our Tech Stack

To build our component, we are going to rely on a modern setup that you are likely already using in your projects.

You will need:

  • A working react environment (Next.js, Vite, or Create React App all work perfectly).
  • Framer Motion: The gold standard for declarative layout transitions. It handles all the heavy mathematical physics under the hood.
  • Tailwind CSS: For incredibly fast, utility-first styling so we don't have to write messy external stylesheet files.

You can install Framer Motion via your terminal using your preferred package manager:

npm install motion
Carousel slide 1

Step 1: Setting Up the Component State

The entire logic of our slider hinges on knowing two specific things at all times:

  1. Which slide (index) is currently visible?
  2. Did the user click "Next" or "Previous"? (We need to know the direction to instruct our animations how to slide the new image onto the screen).

Let's initialize our standard state hooks. We'll use a simple array of images to map over.

import React, { useState } from 'react';
const images = [
"https://images.unsplash.com/photo-1...",
"https://images.unsplash.com/photo-2...",
"https://images.unsplash.com/photo-3...",
];
export default function Carousel() {
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0);
const paginate = (newDirection) => {
setDirection(newDirection);
// Calculate the next index. If we are at the end, jump to 0.
// If we are at the beginning and go backward, jump to the end.
setCurrentIndex((prev) => {
let nextIndex = prev + newDirection;
if (nextIndex < 0) return images.length - 1;
if (nextIndex >= images.length) return 0;
return nextIndex;
});
};
return (
<div className="relative w-full max-w-2xl mx-auto h-[400px] overflow-hidden rounded-2xl bg-zinc-100 flex items-center justify-center">
{/* We will add our images here in Step 2 */}
<button
className="absolute left-4 z-10 p-2 bg-white/80 rounded-full hover:bg-white"
onClick={() => paginate(-1)}
>
Prev
</button>
<button
className="absolute right-4 z-10 p-2 bg-white/80 rounded-full hover:bg-white"
onClick={() => paginate(1)}
>
Next
</button>
</div>
);
}

This sets up our infinite loop logic. Whenever a user clicks the "Prev" or "Next" buttons, we safely update our state index without triggering an out-of-bounds error. It's concise and readable.

Step 2: Adding Framer Motion for Smooth Transitions

Now comes the fun part. Up until now, changing the index would result in an instant, harsh leap to the next photo. We want smooth transitions. Enter AnimatePresence and motion.

AnimatePresence is a wrapper component provided by Framer Motion that allows elements to animate securely before they are removed from the React DOM. This is crucial for sliders because when index 0 leaves and index 1 enters, both need to be animated simultaneously.

Let's define a basic variant dictionary that tells the DOM what properties to apply based on the incoming direction.

import { motion, AnimatePresence } from 'motion/react';
const slideVariants = {
enter: (direction) => {
return {
x: direction > 0 ? 1000 : -1000,
opacity: 0
};
},
center: {
zIndex: 1,
x: 0,
opacity: 1
},
exit: (direction) => {
return {
zIndex: 0,
x: direction < 0 ? 1000 : -1000,
opacity: 0
};
}
};

Notice how the enter and exit states dynamically receive a direction argument? If we are moving forward, the incoming slide starts 1000 pixels to the right, sliding perfectly into the center.

We can then inject our motion.img straight into our component like this:

<AnimatePresence initial={false} custom={direction}>
<motion.img
key={currentIndex}
src={images[currentIndex]}
custom={direction}
variants={slideVariants}
initial="enter"
animate="center"
exit="exit"
transition={{
x: { type: "spring", stiffness: 300, damping: 30 },
opacity: { duration: 0.2 }
}}
className="absolute w-full h-full object-cover"
alt="Carousel slide"
/>
</AnimatePresence>

Look at that transition prop. By defining type as spring, we are completely discarding standard linear CSS timelines for a physics-based response. It will bounce slightly into place naturally without feeling slow. Adding initial={false} on AnimatePresence ensures that our very first image on page-load doesn't awkwardly animate onto the screen—it just starts there cleanly.

Step 3: Handling Autoplay and Pause on Hover

Often, especially on SaaS dashboard landing pages or pricing sections, you want your testimonials to rotate automatically without requiring user input. However, an iron-clad rule of web accessibility is that users must be able to read your text. If they hover their cursor over an interesting image or quote, the rotation must pause.

To solve this we use a useEffect hook to manage a standard JavaScript interval, combined with a isPaused state boolean.

const [isPaused, setIsPaused] = useState(false);
useEffect(() => {
if (isPaused) return;
const timer = setInterval(() => {
paginate(1);
}, 5000); // 5 seconds
return () => clearInterval(timer);
}, [currentIndex, isPaused]);

Then, you wrap your main container div with simple React event handlers: onMouseEnter={() => setIsPaused(true)} and onMouseLeave={() => setIsPaused(false)}. Now, your application perfectly balances automated discovery with user-controlled pacing. When the mouse leaves the container boundaries, the interval resets, and the automated timeline resumes effortlessly.

Step 4: Accessibility Best Practices

Performance and visual flair are important, but ignoring screen-readers and keyboard-friendly navigation means alienating a core percentage of your web traffic. Every custom UI component must be accessible.

A few critical additions you must make:

  1. Ensure your wrapper div has an aria-roledescription="carousel".
  2. Your individual slides should have aria-label identifying which slide it is (e.g., "Slide 1 of 3").
  3. Your next and previous buttons need aria-label="Previous slide" and aria-label="Next slide" attributes. Let them be highly contrasting so it’s visually readable.

By strictly adhering to w3c standards regarding aria labelling, you maintain a healthy, compliant web application that serves all kinds of users reliably.

The Final Polish: Bringing it All Together

When you combine all of these separate features—accessibility standards, physics-driven animations, gesture-drag capabilities, an auto-play interval with hover-pausing, and responsive styling—you construct an incredible engineering accomplishment. Your codebase ends up looking highly polished, with all the logic contained strictly within isolated hooks and declarative components.

However, as a developer evaluating your workload, you might be looking at this timeline and realizing just how much goes into a "simple" image slider. We outlined nearly 200 lines of highly specific code logic, and that doesn't even heavily factor in resizing edge cases, loading spinners, Next.js optimization regarding lazy loading static assets, or edge caching!

The Full Code

If you want to copy the complete component structure exactly as described above, here is the full code. Save this as AnimatedCarousel.tsx:

"use client";
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
const images = [
"https://images.unsplash.com/photo-1555041469-a586c61ea9bc?q=80&w=2070&auto=format&fit=crop",
"https://images.unsplash.com/photo-1550751827-4bd374c3f58b?q=80&w=2070&auto=format&fit=crop",
"https://images.unsplash.com/photo-1542831371-29b0f74f9713?q=80&w=2070&auto=format&fit=crop",
"https://images.unsplash.com/photo-1498050108023-c5249f4df085?q=80&w=2072&auto=format&fit=crop"
];
const slideVariants = {
enter: (direction: number) => {
return {
x: direction > 0 ? 1000 : -1000,
opacity: 0
};
},
center: {
zIndex: 1,
x: 0,
opacity: 1
},
exit: (direction: number) => {
return {
zIndex: 0,
x: direction < 0 ? 1000 : -1000,
opacity: 0
};
}
};
export default function AnimatedCarousel() {
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const paginate = (newDirection: number) => {
setDirection(newDirection);
setCurrentIndex((prev) => {
let nextIndex = prev + newDirection;
if (nextIndex < 0) return images.length - 1;
if (nextIndex >= images.length) return 0;
return nextIndex;
});
};
useEffect(() => {
if (isPaused) return;
const timer = setInterval(() => {
paginate(1);
}, 5000);
return () => clearInterval(timer);
}, [currentIndex, isPaused]);
return (
<div
className="relative w-full max-w-2xl mx-auto h-[400px] overflow-hidden rounded-2xl bg-zinc-900 flex items-center justify-center shadow-xl my-8 group"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
aria-roledescription="carousel"
>
<AnimatePresence initial={false} custom={direction}>
<motion.img
key={currentIndex}
src={images[currentIndex]}
custom={direction}
variants={slideVariants}
initial="enter"
animate="center"
exit="exit"
transition={{
x: { type: "spring", stiffness: 300, damping: 30 },
opacity: { duration: 0.2 }
}}
className="absolute w-full h-full object-cover"
alt={`Carousel slide ${currentIndex + 1}`}
/>
</AnimatePresence>
<button
className="absolute left-4 z-10 p-2 bg-black/20 text-white backdrop-blur-md rounded-full "
onClick={() => paginate(-1)}
aria-label="Previous slide"
>
<ChevronLeft size={24} />
</button>
<button
className="absolute right-4 z-10 p-2 bg-black/20 text-white backdrop-blur-md rounded-full"
onClick={() => paginate(1)}
aria-label="Next slide"
>
<ChevronRight size={24} />
</button>
<div className="absolute bottom-4 z-10 flex gap-2">
{images.map((_, index) => (
<button
key={index}
className={`w-2 h-2 rounded-full transition-colors ${
index === currentIndex ? 'bg-white' : 'bg-white/30'
}`}
onClick={() => {
setDirection(index > currentIndex ? 1 : -1);
setCurrentIndex(index);
}}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
</div>
);
}

The Faster Alternative for Busy Founders: ogBlocks

If you are an indie hacker, an agency developer building multiple sites a week, or a SaaS founder sprinting to ship a minimum viable product, time is your most precious currency. Every hour spent agonizing over the perfect Framer Motion spring coefficient or debugging a bizarre touch-cancel event on an iOS Safari browser is an hour you aren’t talking to users or building core business features.

Writing every single micro-interaction component from absolute zero is often a massive misallocation of your time.

That is exactly why thousands of smart founders actively skip the manual build process and leverage premium UI component libraries. Instead of wrestling with drag constraints and variable states, you can copy, paste, and ship.

If you desperately need a stunning, production-ready, highly tested React animated carousel implemented in the next 10 seconds, you should deeply consider heading over to the ogBlocks Card Components gallery.

The library doesn’t just stop at carousels. It houses a vast suite of pre-engineered, highly aesthetic UI items spanning pricing cards, testimonial blocks, sophisticated navbar layouts, and dynamic cursors. Built entirely on top of React, Framer Motion, and Tailwind CSS, everything seamlessly drops right into your codebase. You retain 100% ownership over the source code. It combines the speed of a low-code UI builder with the unlimited customization of open-source boilerplate.

Stop reinventing the wheel entirely. Start dropping fully animated, pixel-perfect UI elements into your codebase today, and save yourself hundreds of frustrating hours of frontend debugging.


Frequently Asked Questions (FAQ)

What is the best library for a React animated carousel?
Most modern developers avoid outdated, bloated libraries like Slick, and instead choose to build isolated, custom-hook driven components utilizing motion/react to handle hardware-accelerated transitions and physics-based momentum natively.

Is a custom-built solution better than a pre-built template?
While custom-built solutions allow for granular control over weird edge cases, pre-built high-quality component libraries like ogBlocks afford you exactly the same amount of control (since you own the source code) while simultaneously completely eliminating 95% of the strenuous setup workload.

Does an animated carousel slow down my application?
If built poorly using standard top/left CSS properties, absolutely. But if built utilizing hardware accelerated transforms (like translate3d, or framer motion's x/y properties), an animated carousel imposes practically zero structural strain on the browser’s render thread, keeping your app fast and lightweight.

Written by Karan

ogBlocks is an Animated React UI Component library built with Motion and Tailwind CSS

How to Build a Pro React Animated Carousel (Free Source Code) | OGBlocks Blog | ogBlocks