The following approach covers how to create an animated sliding page gallery using framer-motion and ReactJS.
Prerequisites:
- Knowledge of JavaScript (ES6)
- Knowledge of HTML and CSS.
- 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:
- logo.svg
- serviceWorker.js
- setupTests.js
- App.test.js (if any)
- 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:
