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