Building a Sleek and Animated Image Carousel with React and Framer Motion

Image carousels, also known as slideshows or sliders, are a popular feature found on many websites that allow you to display multiple images within a confined space. Rather than taking up precious real estate by showing all the images at once, a carousel rotates through the images one at a time, usually with nice animated transitions in between.

Not only do carousels look great, but they also provide an engaging way for users to browse through visual content without being overwhelmed. When done well, an image carousel can be an effective tool for showcasing products, portfolios, or storytelling through photos.

In this tutorial, I‘ll show you how to build your own image carousel component in React that has smooth, eye-catching animations powered by the Framer Motion library. By the end, you‘ll have a professional-quality, responsive carousel that you can easily integrate into your React projects. Let‘s get started!

What We‘ll Be Building

Here‘s a preview of the image carousel we‘ll be creating:

[Animated GIF of final carousel]

As you can see, it has the following features:

  • Displays one image at a time, filling the available space
  • Smoothly animates between images with sliding transitions
  • Right/left navigation arrows to go to the next/previous image
  • Pagination dots that allow jumping to any image
  • Fully responsive design that adapts to different screen sizes

We‘ll be using React to build the carousel component itself, and Framer Motion to add the polished animations. If you‘re new to either of these, don‘t worry – I‘ll explain the key concepts as we go.

Setting Up the Project

Before we start building the carousel, we need to set up a new React project. The quickest way to do this is using Create React App:

npx create-react-app react-image-carousel
cd react-image-carousel

This will generate a basic React project for us with all the necessary configuration. Next, we need to install Framer Motion:

npm install framer-motion

Framer Motion is a powerful animation library that makes it easy to add fluid animations and gestures to React components. It uses a declarative syntax, similar to CSS, to define how elements should animate.

With our dependencies installed, we‘re ready to start building the carousel component itself. Open up the project in your favorite code editor and let‘s dive in!

Creating the Carousel Component

Inside the src directory, create a new file called Carousel.js. This is where we‘ll write the code for our carousel component. Start by importing React, the useState hook which we‘ll use to manage state, and the motion and AnimatePresence components from Framer Motion:

import React, { useState } from ‘react‘;
import { motion, AnimatePresence } from ‘framer-motion‘;

Next, set up a basic functional component called Carousel that accepts an array of image URLs as a prop:

const Carousel = ({ images }) => {
  const [currentIndex, setCurrentIndex] = useState(0);

  return (
    // JSX goes here 
  );
};

export default Carousel;

We‘re using the useState hook to create a state variable called currentIndex, which will keep track of which image is currently being displayed. We initialize it to 0, meaning the first image in the array will be shown by default.

Now, in the component‘s return statement, let‘s render the current image along with "previous" and "next" buttons for navigation:

return (
  <div className="carousel">
    <img src={images[currentIndex]} alt="carousel image" />

    <div className="carousel-nav">
      <button onClick={handlePrevious}>←</button>  
      <button onClick={handleNext}>→</button>
    </div>
  </div>
);

To determine which image to display, we use images[currentIndex] to grab the image URL at the specified index in the images array.

For the navigation buttons, we‘ve attached onClick handlers called handlePrevious and handleNext, which will be responsible for updating currentIndex to cycle through the images. Let‘s implement those next:

const handlePrevious = () => {
  setCurrentIndex((prevIndex) => 
    prevIndex === 0 ? images.length - 1 : prevIndex - 1
  );
};

const handleNext = () => {
  setCurrentIndex((prevIndex) => 
    prevIndex === images.length - 1 ? 0 : prevIndex + 1  
  );
};

Both of these handler functions use the setCurrentIndex function to update currentIndex based on its previous value.

The handlePrevious function checks if currentIndex is currently 0, meaning we‘re on the first image. If so, it "wraps around" to the last image in the array by setting currentIndex to images.length - 1. Otherwise, it simply decrements currentIndex by 1 to go back one image.

Similarly, handleNext increments currentIndex by 1 to advance to the next image, unless we‘re currently on the last image, in which case it wraps back around to the first one.

With this logic in place, we can now cycle through the images in both directions using the nav buttons! The only problem is, the transition between images is very abrupt. This is where Framer Motion comes in to save the day.

Animating with Framer Motion

To add smooth animations to our carousel, we‘re going to use Framer Motion‘s AnimatePresence and motion components.

First, import the css function at the top of the file, which we‘ll use to define animation styles:

import { motion, AnimatePresence, css } from ‘framer-motion‘;

Then, update the img element to use the motion component and wrap it with AnimatePresence:

<AnimatePresence>
  <motion.img 
    key={images[currentIndex]}
    src={images[currentIndex]}
    alt="carousel image"
  />
</AnimatePresence>  

The AnimatePresence component allows components to animate out when they‘re removed from the tree, which is perfect for transitioning between images in our carousel.

We‘ve also added a key prop to the motion.img, set to the URL of the current image. This is important because it lets Framer Motion know when the image has changed so it can trigger an animation.

Now, let‘s define the actual animations using variants, which are objects that specify target values for animatable properties. Add this code above the return statement:

const variants = {
  initial: (direction) => ({
    x: direction > 0 ? 1000 : -1000,
    opacity: 0,  
  }),
  animate: {
    x: 0, 
    opacity: 1,
    transition: {
      x: { type: ‘spring‘, stiffness: 300, damping: 30 },
      opacity: { duration: 0.2 },
    },
  },
  exit: (direction) => ({
    x: direction > 0 ? -1000 : 1000, 
    opacity: 0,
    transition: {
      x: { type: ‘spring‘, stiffness: 300, damping: 30 },  
      opacity: { duration: 0.2 },
    },
  }),
};  

There‘s a lot going on here, so let‘s break it down:

  • We define 3 variant states: initial, animate, and exit
  • Both initial and exit are functions that return different x offsets based on the direction parameter (more on this in a bit). This allows us to configure the direction the image slides in from and out to.
  • In the animate state, we set x to 0 and opacity to 1, meaning the image will be centered and fully opaque.
  • We specify transition options for x and opacity to customize the animation curves. Here we‘re using a spring curve for the position and a simple duration for the opacity fade.

In order to determine the direction the image should slide in from, we need to keep track of which nav button was clicked. To do this, add a new state variable called direction and update the nav button handlers:

const [direction, setDirection] = useState(0);

const handlePrevious = () => {
  setCurrentIndex((prevIndex) => 
    prevIndex === 0 ? images.length - 1 : prevIndex - 1
  );
  setDirection(-1);
};

const handleNext = () => {  
  setCurrentIndex((prevIndex) => 
    prevIndex === images.length - 1 ? 0 : prevIndex + 1
  );
  setDirection(1);
};

We initialize direction to 0, and then in handlePrevious we set it to -1 and in handleNext we set it to 1. This value then gets passed into our variant functions to conditionally set the initial and exit offsets.

Finally, apply the variants to the motion.img:

<AnimatePresence initial={false} custom={direction}>
  <motion.img
    key={images[currentIndex]} 
    src={images[currentIndex]}
    alt="carousel image"
    variants={variants}
    initial="initial"
    animate="animate"  
    exit="exit"
    custom={direction}
  />
</AnimatePresence>

Note how we pass custom={direction} both to the AnimatePresence component as well as the motion.img. This is how we feed the direction value to the variants.

And with that, our image transitions should now be fully animated! The images will slide in from the correct direction based on which button was clicked and gracefully slide out in the opposite direction.

But we‘re not done yet – let‘s add one more feature to really take this carousel to the next level.

Adding Pagination Dots

It‘s common for image carousels to include pagination dots that allow users to quickly jump to a specific image without having to cycle through them one at a time. Here‘s how we can add this functionality to our carousel:

First, render a set of dots below the image, one for each image in the images array:

<div className="carousel-dots">
  {images.map((_, index) => (
    <button
      key={index}
      onClick={() => handleDotClick(index)}  
      className={currentIndex === index ? ‘active‘ : ‘‘}
    />
  ))}
</div>

We map over the images array and for each one render a button element. The onClick handler will call a new handleDotClick function, passing it the current index. We also conditionally apply an active class if the dot‘s index matches the currentIndex.

Next, let‘s write the handleDotClick function:

const handleDotClick = (index) => {
  setCurrentIndex(index);
  setDirection(index > currentIndex ? 1 : -1);  
};

This function does two things:

  1. Sets currentIndex to the clicked dot‘s index
  2. Sets direction based on whether we‘re jumping forward or backward in the image list, similar to the nav button handlers.

Now, when a pagination dot is clicked, the carousel will smoothly animate to the corresponding image! The dots also highlight the currently active image, providing a clear visual indicator of where you are in the list.

Styling with CSS

At this point, our carousel is fully functional, but it doesn‘t look very pretty. Let‘s add some CSS to clean it up.

Create a new file called Carousel.css and import it at the top of Carousel.js:

import ‘./Carousel.css‘;  

Then, add the following styles:

.carousel {
  position: relative;
  width: 100%;
  max-width: 800px; 
  margin: 0 auto;
}

.carousel img {
  width: 100%;
  height: auto;  
}

.carousel-nav {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  display: flex;
  justify-content: space-between;  
  z-index: 1;
}

.carousel-nav button {
  background: rgba(0, 0, 0, 0.5);
  border: none;
  color: white;  
  font-size: 2rem;
  padding: 0.5rem;
  cursor: pointer;
  transition: opacity 0.3s;
}

.carousel-nav button:hover {
  opacity: 0.8;
}

.carousel-dots {
  position: absolute;  
  bottom: 1rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  justify-content: center;
  z-index: 1;  
}

.carousel-dots button {
  background: rgba(255, 255, 255, 0.5);  
  border: none;
  border-radius: 50%;
  width: 12px;
  height: 12px; 
  margin: 0 0.5rem;
  cursor: pointer;
  transition: background 0.3s;
}

.carousel-dots button.active {
  background: white;
}

These styles do the following:

  • Constrain the carousel to a max width and center it horizontally
  • Make the image fill the carousel container while maintaining aspect ratio
  • Position the nav buttons on either side of the image and style them
  • Position the dots below the image and style them
  • Add hover/active states for the buttons

With these finishing touches, our carousel is now looking much more polished and professional!

Accessibility Considerations

As with any component, it‘s important to ensure our carousel is accessible to all users, including those using assistive technologies. While a full accessibility audit is beyond the scope of this tutorial, here are a few key things to consider:

  • Add alt text to the images to describe their content
  • Ensure the nav and pagination buttons are keyboard-focusable and operable
  • Provide aria labels for the buttons to clarify their purpose
  • Consider adding touch/swipe support for mobile users

Many of these are relatively easy to implement and go a long way towards making the carousel more inclusive.

Conclusion

And there you have it – a complete image carousel component built with React and Framer Motion! We covered a lot of ground in this tutorial, including:

  • Setting up a new React project with Create React App
  • Installing and configuring Framer Motion
  • Creating a reusable Carousel component
  • Rendering a list of images and cycling through them
  • Adding navigation buttons and pagination dots
  • Animating the image transitions with Framer Motion variants
  • Styling the carousel with CSS
  • Discussing accessibility best practices

I encourage you to experiment with the code and make the carousel your own. Try adding different animation effects, customizing the styling, or even extending it with additional features like autoplay or infinite looping.

The complete source code for this tutorial is available on GitHub. Feel free to use it as a starting point for your own projects!

If you have any questions or feedback, reach out in the comments below. Happy coding!

Similar Posts