In this article, we’ll take a closer look at how Framer Motion helps us in creating awesome animations. We’ll learn how motion components work and learn how to chain animations together. We’ll look into how to make gesture-triggered, timed, and scroll animations with Framer motion. Along the way, we’ll use the things we learn to build five demo applications I’ve set up to show us how we can integrate Framer Motion into real-world applications.
This tutorial will be beneficial to readers who are interested in integrating animations in their React application.
Note: This article requires a basic understanding of React and CSS.
What Is Framer Motion?
Framer Motion is an animation library that makes creating animations easy. Its simplified API helps us abstract the complexities behind animations and allows us to create animations with ease.
Motion Components
These are the building blocks of Framer motion. Motion components are created by prefixing motion
to your regular HTML and SVG element (e.g, motion.h1
). Motion components can accept several props, with the basic one being the animate
prop. This prop takes in an object where we define the properties of that component we want to animate. The properties we define will be animated when the component mounts in the DOM.
Let’s animate an h1 text using Framer Motion. First, we install the framer-motion library and import motion
.
npm i framer-motion
import { motion } from 'framer-motion';
Then we convert the h1 into a motion component.
<motion.h1 animate={{x: 20, y: -20}}> This is a motion component
</motion.h1>
This will cause the h1
to slide 20px to the right and move 20px up when it loads. When units aren’t added, calculations are done using pixels. However, you can explicitly set the units you want the calculations to be based on, animate={{x: "20rem", y: "-20rem"}}>
.
By default, a motion component will be animated from the state defined from its styles to those in the animate
prop. However, if we wanted to, we could hijack and define the initial animation state of the component using the initial
prop. While the animate
prop is used to define the behavior of components when they mount, the initial
prop defines their behavior before they mount.
If we want our h1 to come in from the left, we control that using the initial prop.
<motion.h1 initial={{x: -1000}} animate={{x: 20}}> This is a motion component
</motion.h1>
Now, when the h1
mounts, it slides in from the left.
We are not limited to a single animation. We can define a series of animations called keyframes
in an array of values. Each value will get animated in sequence.
<motion.h1 initial={{x: -1000}} animate={{x: [20, 50, 0, -70, 40] }}> This is a motion component
</motion.h1>
The transition
prop allows us to define how the animations occur. With it, we define how values animate from one state to another. Among other things, we can define the duration
, delay
, and type
of animation using this prop.
<motion.h1 initial={{ x: -1000 }} animate={{ x: 0 }} transition={{ type: "tween", duration: "2", delay: "1" }}> This is a motion component
</motion.h1>
Say we were to animate several motion components simultaneously, like in the code snippet below.
<div className="App"> <motion.h1 initial={{ x: -1000 }} animate={{ x: 0 }} transition={{ type: "tween", duration: "2", delay: "1" }}> This is a motion h1 </motion.h1> <motion.h2 initial={{ y: -1000 }} animate={{ y: 0 }} transition={{ type: "tween", duration: "1", delay: ".4" }}>This is a motion h2 </motion.h2> <motion.h3 initial={{ x: 100, opacity: 0 }} animate={{ x: 0, opacity: 1 }}> This is a motion h3 </motion.h3> <motion.h4 initial={{ scale: 0.7 }} animate={{ scale: 1.7 }} transition={{ type: "tween", duration: "2", delay: "1" }}> This is a motion h4 </motion.h4> </div>
While this works, the variants
prop in Framer Motion enables us to extract our animation definitions into a variants object. Not only do variants
make our code cleaner, but they allow us to create even more powerful and complex animations.
Extracting our animation definitions into variants objects, we have this:
const H1Variants = { initial: { x: -1000 }, animate: { x: 0 }, transition: { type: "tween", duration: 2, delay: 1 }
} const H2Variants = { initial: { y: -1000 }, animate: { y: 0 }, transition: { type: "tween", duration: 1, delay: .4 }
}
const H3Variants = { initial:{ x: 100, opacity: 0 }, animate:{ x: 0, opacity: 1 }
}
const H4Variants = { initial:{ scale: 0.7 }, animate:{ scale: 1.7 }, transition:{ type: "tween", duration: "2", delay: "1" }
}
Instead of passing the animation definitions into a component’s initial
and animate
props directly, we extract these definitions into standalone variant objects. In the variant objects, we define variant names that describe each animation’s name as variants.
<div className="App"> <motion.h1 variants={H1Variants} initial='initial' animate='animate' > This is a motion h1 </motion.h1> <motion.h2 variants={H2Variants} initial='initial' animate='animate' > This is a motion h2 </motion.h2> <motion.h3 variants={H3Variants} initial='initial' animate='animate' > This is a motion h3 </motion.h3> <motion.h4 variants={H4Variants} initial='initial' animate='animate' > This is a motion h4 </motion.h4>
</div>
In the variants
prop, we pass in the name of the variant objects for each motion component and then pass in the animations to the initial
and animate
props.
We can take our current setup with variants further to reduce repetition. Using variants, we can propagate animation attributes down through the DOM from a parent motion component. For this to work, we create variants for the parent motion.div
with similar animation names in its variant object as its children. By doing this, we won’t have to pass the animation names’ to each child component. Behind the scenes, the parent element handles that for us.
const ContainerVariants = { initial: {}, animate: {}
};
const H1Variants = { initial: { x: -1000 }, animate: { x: 0 }, transition: { type: "tween", duration: 2, delay: 1 }
};
//more variants below <motion.div className="App" variants={ContainerVariants} initial="initial" animate="animate" > <motion.h1 variants={H1Variants}>This is a motion h1</motion.h1> <motion.h2 variants={H2Variants}>This is a motion h2</motion.h2> <motion.h3 variants={H3Variants}>This is a motion h3</motion.h3> <motion.h4 variants={H4Variants}>This is a motion h4</motion.h4>
</motion.div>
Now we have a cleaner code with no repetitions. We turned the container div to a motion component so we could pass in the ContainerVariants
object we defined. Since we don’t define any animations on the container, we pass in empty objects to initial
and animate
. Your animation names must be the same in every variant object for the propagation to work.
Now we understand the basics of Framer Motion. Let’s begin building our fist of 5 demo applications.
Icon Shop
We can create interactive animations based on gestures. Motion components are currently able to listen for hover, tap, pan, and drag gesture detection. We’ll be building this Icon Shop app using the whileHover
prop.
Components
App.js
: this holds the heading texts.Card.jsx
: here, we define the animations for the icon cards.CardContainer.jsx
: we import and loop through the icons.styles.js
: create, style, and export the motion components. I used styled-components for styling the components.
Let’s start with App.js
.
import { H1, H2 } from "./Styles";
import CardContainer from "./CardContainer"; return ( <div> <H1 initial={{ y: -100 }} animate={{ y: 0, transition: { delay: 1 } }}> Icon Shop </H1> <H2 initial={{ x: -1000 }} animate={{ x: 0, transition: { delay: 1 } }}> Hover over the cards to see the motion magic </H2> <CardContainer /> </div> );
We import the H1
and H2
motion components we created in the Styles.js
file. Since they are motion components, we use the initial
and animate
props to define their behavior before and when they mount. Here, we also import and display the CardContiner
component.
Now, the CardContainer.js
.
import { Container } from "./Styles";
import Card from "./Card";
import { ReactComponent as AddIcon } from "./assets/add.svg";
import { ReactComponent as AirplaneIcon } from "./assets/airplane.svg";
import { ReactComponent as AlarmIcon } from "./assets/alarm.svg";
//more svg imports below... const icons = [ <AddIcon />, <AirplaneIcon />, <AlarmIcon />, //more icons below
]; const CardContainer = () => { return ( <Container initial={{ x: -1000 }} animate={{ x: 0 }}> {icons.map((icon) => ( <Card icon={icon} /> ))} </Container> );
};
Here, we import the SVGs, the Container
motion component, and the Card
component.
Similar to H1
and H2
in App.js
, we define animations of the Container
using the initial
and animate
props. When it loads, it will create a cool effect of sliding in from the left of the browser.
Now, Card.js
import { CardBox, IconBox } from "./Styles";
const CardVariants = { beforeHover: {}, onHover: { scale: 1.1 }
};
const IconVariants = { beforeHover: { opacity: 0, y: -50 }, onHover: { opacity: 1, y: 0, scale: 1.5, transition: { type: "tween" } }
}; const Card = ({ icon }) => { console.log(icon); return ( <CardBox variants={CardVariants} initial="beforeHover" whileHover="onHover"> <IconBox variants={IconVariants}>{icon}</IconBox> </CardBox> );
};
Here, we create two variant objects with beforeHover
and onHover
animations. In the CardVariants
object, we don’t want to do anything initially, so beforeHover
is an empty object. onHover
we increase the scale of the card box.
In the IconVariants
object, we define the initial state of the IconBox
in its beforeHover
. We set its opacity to 0 and push it upwards by 50px. Then, in onHover
, we set the opacity back to 1, push it back to its default position, and change the transition type to tween
. Then we pass in the variants to their respective motion components. We make use of the propagation, so we don’t need to explicitly set the initial
and animate
props to the IconBox
component.
Animated Navbar
We’ll build a simple Navigation component, and we’ll see how we can create timing relationships between parent and children motion components.
Components
App.js
: this holds the heading texts.Styles.js
: create, style, and export the motion components. The components are styled using styled-components.
Let’s take a look at the App.js
file.
import { Header, Nav, Link, SvgBox } from "./Styles"; function App() { const [isOpen, setIsOpen] = useState(false); const iconVariants = { opened: { rotate: 135 }, closed: { rotate: 0 } }; const menuVariants = { opened: { top: 0, transition: { when: "beforeChildren", staggerChildren: 0.5 } }, closed: { top: "-90vh" } }; const linkVariants = { opened: { opacity: 1, y: 50 }, closed: { opacity: 0, y: 0 } };
We create an isOpen
state that will be used to check if the Navbar is open or not. We create 3 variant objects, iconVariants
, menuVariants
, and linkVariants
where we define the animations for the SvgBox
, Nav
, and Link
motion components respectively. The iconVariants
is used to rotate the SvgBox
135deg when it’s hovered over. We don’t need to add “deg” to the value. In the menuVariants
, we control the top position of the Nav
like you would using the position
property in CSS. We toggle the top position of the Nav
based on the isOpen
state.
With variants, we can create timing relationships between parent and children motion components. We define the relationship between parent Nav
and its child, Link
using the when
property in the transition object. Here, set it to beforeChildren
, so the parent component’s animations will finish before the child’s animation begins.
Using the staggerChildren
property, we set a timing order for each link. Each link will take 0.5 seconds to appear one after the other. This creates a nice visual cue when the Nav
is opened. In the linkVariants
we animate the opacity and the vertical position of each link.
<div className="App"> <Header> <SvgBox variants={iconVariants} animate={isOpen ? "opened" : "closed"} onClick={() => setIsOpen(!isOpen)} > <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" > <path d="M12 4C11.4477 4 11 4.44772 11 5V11H5C4.44772 11 4 11.4477 4 12C4 12.5523 4.44772 13 5 13H11V19C11 19.5523 11.4477 20 12 20C12.5523 20 13 19.5523 13 19V13H19C19.5523 13 20 12.5523 20 12C20 11.4477 19.5523 11 19 11H13V5C13 4.44772 12.5523 4 12 4Z" fill="#fff" /> </svg> </SvgBox> </Header> <Nav initial={false} variants={menuVariants} animate={isOpen ? "opened" : "closed"} > <Link variants={linkVariants}>home</Link> <Link variants={linkVariants}>about</Link> <Link variants={linkVariants}>gallery</Link> </Nav>
</div>
Here, we pass in the variants to their respective components. In the SvgBox
, we toggle the state of isOpen
whenever it’s clicked, then conditionally animate it based on the state. Like the SvgBox
, we conditionally animate the Nav
and the Link
s based on isOpen
’s state.
Animated Modal
We’ll build a modal component and learn about Framer Motion’s AnimatePresence
, and how it allows us animate elements as they leave the DOM.
Components:
App.js
: we set up theshowModal
state here.Modal.jsx
: the actual animation work takes place here.Styles.js
: create, style, and export the motion components. The components are styled using styled-components.
Let’s look into App.js
import { ToggleButton, Container } from "./Styles";
import Modal from "./Modal"; function App() { const [showModal, setShowModal] = useState(false); const toggleModal = () => { setShowModal(!showModal); }; return ( <Container> <ToggleButton initial={{ x: -700 }} animate={{ x: 0, transition: { duration: 0.5 } }} onClick={toggleModal} > Toggle Modal </ToggleButton> <Modal showModal={showModal} /> </Container> );
}
We create a showModal
state that will be used to conditionally render the modal. The toggleModal
function will toggle the state whenever the ToggleButton
is clicked. ToggleButton
is a motion component, so we can define animations for it. When it mounts, it slides in from the left. This animation runs for 0.5 seconds. We also pass in the showModal
state to the Modal
through props.
Now, Modal.jsx
import { AnimatePresence } from "framer-motion";
import { ModalBox, ModalContent, Container } from "./Styles"; <Container> <AnimatePresence> {showModal && ( <ModalBox initial={{ opacity: 0, y: 60, scale: 0.3 }} animate={{ opacity: 1, y: 0, scale: 1, transition: { type: "spring", stiffness: 300 } }} exit={{ opacity: 0, scale: 0.5, transition: { duration: 0.6 } }} > <ModalContent initial={{ y: -30, opacity: 0 }} animate={{ y: 0, opacity: 1, transition: { delay: 1 } }} > Modal content!!!! </ModalContent> </ModalBox> )} </AnimatePresence>
</Container>
We import AnimatePresence
from framer-motion
. It allows us to set exit animations for components when they leave DOM. We conditionally render the Modal
based on the showModal
state. We define the animations for the ModalBox
and ModalContent
through their initial
and animate
props. There’s also a new prop here, exit
. Having AnimatePresence
as a wrapper allows us to add exit animations to ModalBox
in the exit
prop.
Scroll Animation
We’ll use a combination of the useAnimation
hook and react-intersection-observer
to create scroll-triggered animations.
Components
App.js
: we set up the animations for theBox
component and render it inApp
Styles.js
: create, style, and export the motion components. The components are styled using styled-components.
import React, { useEffect } from "react";
import { useAnimation } from "framer-motion";
import { useInView } from "react-intersection-observer";
import { Container, H1,StyledBox } from "./Styles"; const BoxVariants = { visible: { opacity: 1, x: 0, transition: { duration: 1 } }, hidden: { opacity: 0, x: 300 },
}; const Box = () => { const controls = useAnimation(); const [ref, inView] = useInView(); useEffect(() => { if (inView) { controls.start("visible"); } }, [controls, inView]); return ( <StyledBox ref={ref} animate={controls} initial="hidden" variants={BoxVariants} /> );
};
The useAnimation
hook allows us to control the sequences in which our animations occur. We have access to controls.start
and controls.stop
methods that we can use to manually start and stop our animations. We pass in the initial hidden
animaton to StyledBox
. We pass in the controls we defined with the start
method to StyledBox
animate prop.
react-intersection-observer
’s useInView
hook allows us to track when a component is visible in the viewport. The useInView
hook gives us access to ref
, which we pass to the component we want to watch, and the inView
boolean, that tells us if that element is inView
or not. We use the useEffect
to call controls.start
whenever the element we’re watching, StyledBox
is in view. We pass in controls
and inView
as useEffect
’s dependencies. Also, we pass in the variants we defined, BoxVariants
to StyledBox
.
Hero Animation
We’ll build a cool hero banner animation using the useCycle
hook. We’ll understand how useCycle
allows us to cycle through animations.
import React, { useEffect } from "react";
import { useCycle } from "framer-motion";
import { Container, H1, HeroSection, Banner, TextBox } from "./Styles";
import { ReactComponent as BannerIllustration } from "./bighead.svg"; const H1Variants = { initial: { y: -200, opacity: 0 }, animate: { y: 0, opacity: 1, transition: { delay: 1 } },
};
const TextVariants = { initial: { x: 400 }, animate: { x: 0, transition: { duration: 0.5 } },
};
const BannerVariants = { animationOne: { x: -250, opacity: 1, transition: { duration: 0.5 } }, animationTwo: { y: [0, -20], opacity: 1, transition: { yoyo: Infinity, ease: "easeIn" }, },
};
We define 3 variants, H1Variants
, TextVariants
, and BannerVariants
. However, our focus is BannerVariants
. We define 2 animations, animationOne
and animationTwo
in BannerVariants
. These are the animations we pass into the useCycle
to cycle through.
const [animation, cycleAnimation] = useCycle("animationOne", "animationTwo"); useEffect(() => { setTimeout(() => { cycleAnimation(); }, 2000); }, []);
useCycle
works similar to the useState
hook. In the destructured array, animation
represents the animation that is active, whether animationOne
or animationTwo
. The cylceAnimation
function that cycles between the animation we defined. We pass in the animations we want to cycle through into useCycle
and call cylceAnimation
after 2 seconds in useEffect
.
<div className="App"> <Container> <H1 variants={H1Variants} initial="initial" animate="animate"> Cool Hero Section Anmiation </H1> <HeroSection> <TextBox variants={TextVariants} initial="initial" animate="animate"> Storage shed, troughs feed bale manure, is garden wheat oats at augers. Bulls at rose garden cucumbers mice sunflower wheat in pig. Chainsaw foal hay hook, herbs at combine harvester, children is mallet. Goat goose hen horse. Pick up truck livestock, pets and storage shed, troughs feed bale manure, is garden wheat oats at augers. Lamb. </TextBox> <Banner variants={BannerVariants} animate={animation}> <BannerIllustration /> </Banner> </HeroSection> </Container> </div>
At the end of everything, we pass in the variants to their respective components and watch the magic happen. With this, the Banner
will initially slide in from the right based on the animations we defined in animationOne
, and after 2 seconds, cycleAnimation
will be called which will trigger animationTwo
.
As a wise Pig once said, “that’s all folks.”
Conclusion
We’ve gone through the basics of Framer Motion and seen some demo projects that give us a glimpse of the range of animations we can create. However, you can do so much more with it. I encourage you to dive into the docs and go wild.
Resources
(ks, ra, yk, il)