Thursday, October 2, 2025
HomeLanguagesAnimated sliding page gallery using framer-motion and React.js

Animated sliding page gallery using framer-motion and React.js

The following approach covers how to create an animated sliding page gallery using framer-motion and ReactJS.

Prerequisites:

  1. Knowledge of JavaScript (ES6)
  2. Knowledge of HTML and CSS.
  3. Basic knowledge of ReactJS.

Creating React Application And Installing Module:

Step 1: Create a React application using the following command:

$ npx create-react-app page-gallery

Step 2: After creating your project folder i.e. page-gallery, move to it using the following command.

$ cd page-gallery

Step 3: Add the npm packages you will need during the project.

$ npm install framer-motion @popmotion/popcorn

Open the src folder and delete the following files:

  1. logo.svg
  2. serviceWorker.js
  3. setupTests.js
  4. App.test.js (if any)
  5. index.css.

Create a file named PageSlider.js.

Project structure: The project structure tree should look like this:

Project structure

Filename: App.js

Javascript




import React from "react";
import { useState } from "react";
import { motion, AnimateSharedLayout } from "framer-motion";
import PageSlider from "./PageSlider";
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 (
    <>
      <PageSlider
        currentPage={currentPage}
        direction={direction}
        setPage={setPage}
      />
      <Pagination currentPage={currentPage}
                  setPage={setPage} />
    </>
  );
};
 
export default App;


Filename: PageSlider.js

Javascript




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 PageSlider = ({ 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 PageSlider;


Filename: App.css

CSS




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;
}


Step to Run Application: Run the application using the following command from the root directory of the project:

$ npm start

Output: Now open your browser and go to http://localhost:3000/, you will see the following output:

Whether you’re preparing for your first job interview or aiming to upskill in this ever-evolving tech landscape, neveropen Courses are your key to success. We provide top-quality content at affordable prices, all geared towards accelerating your growth in a time-bound manner. Join the millions we’ve already empowered, and we’re here to do the same for you. Don’t miss out – check it out now!
RELATED ARTICLES

Most Popular

Dominic
32330 POSTS0 COMMENTS
Milvus
85 POSTS0 COMMENTS
Nango Kala
6703 POSTS0 COMMENTS
Nicole Veronica
11867 POSTS0 COMMENTS
Nokonwaba Nkukhwana
11926 POSTS0 COMMENTS
Shaida Kate Naidoo
6817 POSTS0 COMMENTS
Ted Musemwa
7078 POSTS0 COMMENTS
Thapelo Manthata
6775 POSTS0 COMMENTS
Umr Jansen
6774 POSTS0 COMMENTS