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:
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: