DEV Community

Satel Dev
Satel Dev

Posted on

Animated Carousel with Framer Motion

I'd like to create Carousel component with Framer motion in React application.

Prerequisites:

  1. Knowledge of Javascript(ES6)
  2. Knowledge of HTML and CSS
  3. Basic knowledge of React

Creating React Application and Installing Npm Modules

In your bash, or terminal run the below commands

npx create-react-app carousel
Enter fullscreen mode Exit fullscreen mode
cd carousel && npm install  framer-motion @popmotion/popcorn
Enter fullscreen mode Exit fullscreen mode

Update the App Component

import React from "react";
import { useState } from "react";
import { motion, AnimateSharedLayout } from "framer-motion";
import Carousel from "./Carousel";
import "./styles.css";

const Pagination = ({ currentPage, setPage }) => {
  // Wrap all the pagination Indicators
  // with AnimateSharedPresence
  // so we can detect when Indicators
  // with a layoutId are removed/added
  return (
    <AnimateSharedLayout>
      <div className="Indicators">
        {pages.map((page) => (
          <Indicator
            key={page}
            onClick={() => setPage(page)}
            isSelected={page === currentPage}
          />
        ))}
      </div>
    </AnimateSharedLayout>
  );
};

const Indicator = ({ isSelected, onClick }) => {
  return (
    <div className="Indicator-container" onClick={onClick}>
      <div className="Indicator">
        {isSelected && (
          // By setting layoutId, when this component
          // is removed and a new one is added elsewhere,
          // the new component will animate out from the old one.
          <motion.div className="Indicator-highlight"
                      layoutId="highlight" />
        )}
      </div>
    </div>
  );
};

const pages = [0, 1, 2, 3, 4];

const App = () => {
  /* We keep track of the pagination direction as well as
   * current page, this way we can dynamically generate different
   * animations depending on the direction of travel */
  const [[currentPage, direction], setCurrentPage] = useState([0, 0]);

  function setPage(newPage, newDirection) {
    if (!newDirection) newDirection = newPage - currentPage;
    setCurrentPage([newPage, newDirection]);
  }

  return (
    <>
      <Carousel
        currentPage={currentPage}
        direction={direction}
        setPage={setPage}
      />
      <Pagination currentPage={currentPage}
                  setPage={setPage} />
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Creating Carousel Component

Add Carousel.js in the src folder of the App, and update the code with below.

import React from "react";
import { useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { wrap } from "@popmotion/popcorn";

// Variants in framer-motion define visual states
// that a rendered motion component can be in at
// any given time.

const xOffset = 100;
const variants = {
  enter: (direction) => ({
    x: direction > 0 ? xOffset : -xOffset,
    opacity: 0
  }),
  active: {
    x: 0,
    opacity: 1,
    transition: { delay: 0.2 }
  },
  exit: (direction) => ({
    x: direction > 0 ? -xOffset : xOffset,
    opacity: 0
  })
};

const pages = [0, 1, 2, 3, 4];

const Carousel = ({ currentPage, setPage, direction }) => {
  /* Add and remove pages from the array to checkout
     how the gestures and pagination animations are
     fully data and layout-driven. */
  const hasPaginated = useRef(false);

  function detectPaginationGesture(e, { offset }) {
    if (hasPaginated.current) return;
    let newPage = currentPage;
    const threshold = xOffset / 2;

    if (offset.x < -threshold) {
      // If user is dragging left, go forward a page
      newPage = currentPage + 1;
    } else if (offset.x > threshold) {
      // Else if the user is dragging right,
      // go backwards a page
      newPage = currentPage - 1;
    }

    if (newPage !== currentPage) {
      hasPaginated.current = true;
      // Wrap the page index to within the
      // permitted page range
      newPage = wrap(0, pages.length, newPage);
      setPage(newPage, offset.x < 0 ? 1 : -1);
    }
  }

  return (
    <div className="slider-container">
      <AnimatePresence
        // This will be used for components to resolve
        // exit variants. It's necessary as removed
        // components won't rerender with
        // the latest state (as they've been removed)
        custom={direction}>
        <motion.div
          key={currentPage}
          className="slide"
          data-page={currentPage}
          variants={variants}
          initial="enter"
          animate="active"
          exit="exit"
          drag="x"
          onDrag={detectPaginationGesture}
          onDragStart={() => (hasPaginated.current = false)}
          onDragEnd={() => (hasPaginated.current = true)}
          // Snap the component back to the center
          // if it hasn't paginated
          dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
          // This will be used for components to resolve all
          // other variants, in this case initial and animate.
          custom={direction}
        />
      </AnimatePresence>
    </div>
  );
};

export default Carousel;
Enter fullscreen mode Exit fullscreen mode

Styling

Update the App.css with the simple styling for carousel.

body {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  overflow: hidden;
  background: #09a960;
}

* {
  box-sizing: border-box;
}

.App {
  font-family: sans-serif;
  text-align: center;
}

.slider-container {
  position: relative;
  width: 600px;
  height: 600px;
}

.slide {
  border-radius: 5px;
  background: white;
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}

/* position of indicator container */
.Indicators {
  display: flex;
  justify-content: center;
  margin-top: 30px;
}

.Indicator-container {
  padding: 20px;
  cursor: pointer;
}

.Indicator {
  width: 10px;
  height: 10px;
  background: #fcfcfc;
  border-radius: 50%;
  position: relative;
}

.Indicator-highlight {
  top: -2px;
  left: -2px;
  background: #09f;
  border-radius: 50%;
  width: 14px;
  height: 14px;
  position: absolute;
}
Enter fullscreen mode Exit fullscreen mode

Run the App

npm start
Enter fullscreen mode Exit fullscreen mode

You could see simple animated Carousel in your localhost:3000.

Thanks

Discussion (0)