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 : 100 vh; Â Â 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: