In this guide we will apply Framer Motion animations to a basic example project to enrich the user experience with animations.
The project is made with Next and TypeScript, but you can apply all the concepts to a normal React project with JavaScript.
Sample Project
You can find in this repo the sample project, a basic memory game that has different screens for introduction, selecting the difficulty, selecting the deck (with different animes to play) and the game itself. As in other memory games, you have to discover all the pairs within the time limit.
The best approach to following this guide is to use the initial version which is fully functional without animations, test the different sections of code in the article and review the final version if you had any problems during the process.
You can check a live demo of the sample project:
*In this version, CSS animations are added to at least make the game playable.
What is Framer Motion?
It's an animation library for React made by Framer that aims to allow us to write animations declaratively and effortlessly with seamless integration with our React ecosystem.
You can achieve the same results using pure CSS but Framer Motion will allow you to quickly introduce nice and smooth animations while keeping your code simpler, working with props as you are used to in React and giving you the possibility to react to state changes and other React behaviours.
Also, if you're not quite used to CSS animations this can be a good introduction to them with a more developer-friendly syntax thanks to the intuitive syntax we'll be using.
You will be able to run simple and complex animations, transitions and even sequential animations with a couple of props in your currently working components.
Installation
Simply install the framer-motion
package in the project:
yarn add framer-motion
npm install framer-motion
Once installed, simply import the motion
component and use it in any HTML tag:
import { motion } from "framer-motion"
<motion.div animate={{ scale: 0.5 }} />
Motion will wrap all HTML elements and add animation properties that we will see throughout this guide.
Basic Animations
As we have seen previously, adding an animation is as simple as using the animate
property on a component wrapped with motion
.
So, as a first test, let's animate the Play
button located on the Intro
page.
// components/Intro
import { motion } from 'framer-motion'
const Intro = ({ next }: { next: () => void }) => {
return (
<div className="flex-vertical">
<h1>Memory Game</h1>
<motion.button
onClick={next}
animate={{ scale: 1.5 }}
transition={{ delay: 1 }}
>
Play
</motion.button>
</div>
)
}
export default Intro
- We wrapped the
button
tag with themotion
component, this allows us to use additional properties such asanimate
. - The animation provided is for scaling up by 1.5
- To be able to see the size difference we add an additional property
transition
, which we will see in detail later, to delay the animation by 1 second.
With those few lines we have an animation ready. For now we're using the JS object syntax we're used to, but later we'll see more options for passing animations in the animate
property.
In the example above, framer motion defaults us to an initial
property with all the default values, but we can define it and override whatever we want for the different states of the animation.
// components/Intro
import { motion } from 'framer-motion'
const Intro = ({ next }: { next: () => void }) => {
return (
<div className="flex-vertical">
<h1>Memory Game</h1>
<motion.button
onClick={next}
initial={{ rotate: -360, scale: 3 }}
animate={{ rotate: 0, scale: 1 }}
transition={{ duration: 1 }}
>
Play
</motion.button>
</div>
)
}
export default Intro
With that we switch from a big Play button to a normal size button while rotating.
Transitions
We will use transitions to control the animation between states, for example in the last example we have delayed the starting point by 1 second but we can do much more.
We are going to change the last Play button a bit to test some of the possibilities that transitions offer, for example we want the animation to scale in an infinite loop instead of just firing once.
// components/Intro
import { motion } from 'framer-motion'
const Intro = ({ next }: { next: () => void }) => {
return (
<div className="flex-vertical">
<h1>Memory Game</h1>
<motion.button
onClick={next}
animate={{ scale: 1.5 }}
transition={{
duration: 0.4,
yoyo: Infinity,
}}
>
Play
</motion.button>
</div>
)
}
export default Intro
- We have removed the delay prop but it will work with it as well.
- Now the duration of 0.4 seconds is the total duration of the animation.
- Finally
yoyo
is a special property to go back and forth between the initial state and the animation, in this case, an infinite number of times. With this property you can control how many times you want to trigger an animation.
Transitions allow us to define the type of animation we want to use, we can use:
-
Tween
→ Animations that are based on time duration, when you define aduration
without any type, this is the default type used.
// components/Intro
<motion.button
onClick={next}
animate={{ rotate: 360 }}
transition={{
type: 'tween',
duration: 0.4,
}}
>
Play
</motion.button>
-
Spring
→ Simulates natural physics as animations, if you have tried react-spring this follows the same principle.
// components/Intro
<motion.button
onClick={next}
initial={{ x: '100vw' }}
animate={{ x: 0 }}
transition={{
type: 'spring',
stiffness: 300,
}}
>
Play
</motion.button>
-
Inertia
→ Such animations will decelerate from an initial speed.
// components/Intro
<motion.button
onClick={next}
animate={{ rotate: 360 }}
transition={{ type: 'inertia', velocity: 450 }}
>
Play
</motion.button>
Try these different options in the sample project and check the resulting animations.
Tip: Some of the above settings are incompatible with some properties, if you use TypeScript, errors will appear if any combination doesn't make sense.
Another useful use of transitions is orchestrations, which we will explain later, but there are a few things to know first.
Variants
As you can see, the code is getting bigger and bigger and soon, these new props will have even more relevance than those related to React logic. We can use variants
to isolate code related to animations and much more.
With variants we need to specify different tags that we will assign to different stages of animations.
Let's refactor one of the Play button examples with variants:
// components/Intro
import { motion } from 'framer-motion'
const buttonVariants = {
hidden: {
x: '100vw',
},
visible: {
x: 0,
transition: {
type: 'spring',
stiffness: 300,
},
},
}
const Intro = ({ next }: { next: () => void }) => {
return (
<div className="flex-vertical">
<h1>Memory Game</h1>
<motion.button
onClick={next}
initial="hidden"
animate="visible"
variants={buttonVariants}
>
Play
</motion.button>
</div>
)
}
export default Intro
Now we replaced all the code inside the component with:
- The tag related to the
initial
state, in this casehidden
(you can name it anything you want). - The tag related to the
animate
state (also contains the transition details). - The
variants
object that this component uses.
Tip: You can move all variants to a separate file as you would do with normal CSS or any other CSS-in-JS library to simplify your component.
Tip: If the parent component and the children share the same tags, you only need to write it once in the parent, the children will have the same tags by default.
Orchestration
In some cases we want to trigger the animations one after the other, in which case orchestration + variants will come in handy.
For example, we will animate the title of the deck selection and once the animation is finished, we will make animations for each of the children.
// components/SelectDeck
import { motion } from 'framer-motion'
import { DECKS } from '@/utils/Decks'
import Button from '../ListedButton'
import { childVariants, containerVariants } from './SelectDeck.variants'
type Props = {
next: () => void
setDeck: (deckName: string) => void
}
const SelectDeck: React.FC<Props> = ({ next, setDeck }) => {
const handleSelect = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
setDeck(event.currentTarget.value)
next()
}
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
<h2>Select Deck</h2>
<div className="flex-vertical stack">
{Object.keys(DECKS).map((theme: string) => (
<motion.div key={theme} variants={childVariants}>
<Button onClick={handleSelect} value={theme}>
{theme}
</Button>
</motion.div>
))}
</div>
</motion.div>
)
}
export default SelectDeck
Before going through the variant code, note that in this component, the motion component container
has the initial
and animated
props defined but the motion children
does not. As mentioned above, the children get the animation props from the parent by default, so if we set the same tags there is no need to specify others.
// components/SelectDeck//SelectDeck.variants.ts
const containerVariants = {
hidden: {
opacity: 0,
x: '100vw',
},
visible: {
opacity: 1,
x: 0,
transition: {
type: 'spring',
mass: 0.4,
damping: 8,
when: 'beforeChildren',
staggerChildren: 0.4,
},
},
}
const childVariants = {
hidden: {
opacity: 0,
},
visible: {
opacity: 1,
},
}
export { containerVariants, childVariants }
- In
transition
we define two props that define the orchestrationwhen
andstaggerChildren
.- In this case, we specify
beforeChildren
so that the parent's animation runs and completes before the children's animation. - The
staggerChildren
parameter will apply each child animation one by one with a 0.4 sec delay between them.
- In this case, we specify
Other ways of orchestration are:
- Using
delay
as we did in the first example. - Delaying the children's animation with
delayChildren
instead of making it depend on the parent animation. - Repeating animations with
repeat
.
With orchestration you can make powerful combinations.
Gestures
In addition to React's built-in listeners, framer motion includes gestures that allow us to perform animations in other situations such as hover
, tap
, pan
, viewport
and drag
.
For example, let's go back to our Play button in the intro screen, and perform other animations when we mouse over and tap the button:
// components/Intro
import { motion } from 'framer-motion'
const buttonVariants = {
hidden: {
x: '100vw',
},
visible: {
x: 0,
transition: {
type: 'spring',
stiffness: 300,
},
},
hover: {
scale: 1.5,
},
tap: {
scale: 0.5,
},
}
const Intro = ({ next }: { next: () => void }) => {
return (
<div className="flex-vertical">
<h1>Memory Game</h1>
<motion.button
onClick={next}
initial="hidden"
animate="visible"
whileHover="hover"
whileTap="tap"
variants={buttonVariants}
>
Play
</motion.button>
</div>
)
}
export default Intro
- We add the
whileHover
andwhileTap
listeners to the newhover
andtap
variants, as always you can name it whatever you want. With these changes, now when we mouse over the button it will scale up and when we click it, it will scale down.
You don't need to use variants to use the gestures, as in the previous examples, you can place the object directly on the listeners instead of the tag.
In this example we are only modifying the scale, but you can make complex animations and even transitions like the ones you have seen so far, think of the gestures as just another state in the animation chain.
Another very useful gesture is whileInView
, with which you can easily control the triggering of animations when an element appears in the viewport, in one of my last articles about how to use Redux Toolkit I made an example project that uses this feature:
// components/Card/Card.tsx
<motion.div
initial="hidden"
variants={cardVariants}
animate={controls}
whileInView="show"
viewport={{ once: true }}
>
...
</motion.div>
*I simplified this component for this article but you can see the actual code in the link above.
Using whileInView
and passing in the variant we want to run is all we need to trigger the animations at that precise moment. We also use viewport
once
to trigger the animation only once and not every time this element returns to the view.
Keyframes
Another way to have more control over the behaviour of the animation is to make it with keyframes, this is the way to go when you want to combine different properties and have an exact control over the values in time.
For example, let's add an animation for the cards when they are placed on the board:
// components/Card/
import { motion } from 'framer-motion'
import { Card as TCard } from '@/types'
import styles from './Card.module.css'
const cardVariants = {
hidden: { scale: 0, rotate: 0 },
flip: {
scale: [1, 0.5, 0.5, 1],
rotate: [0, 180, 360, 0],
transition: {
duration: 0.8,
},
},
}
type Props = {
card: TCard
handleSelection: (card: TCard) => void
flipped: boolean
disabled: boolean
}
export default function Card({
card,
handleSelection,
flipped,
disabled,
}: Props) {
const handleClick = () => {
if (!disabled) handleSelection(card)
}
return (
<motion.div
className={styles.card}
variants={cardVariants}
initial="hidden"
animate="flip"
>
<div className={`${styles.inner} ${flipped ? styles.flipped : ''}`}>
<img className={styles.front} src={card.imageURL} alt="card front" />
<img
src={`${card.imageURL.split('/').slice(0, -1).join('/')}/cover.jpg`}
alt="card back"
className={styles.back}
onClick={handleClick}
/>
</div>
</motion.div>
)
}
Changes made:
- Converted to
motion
div the container and addedcardVariants
,hidden
andflip
states. - In
cardVariants
instead of using a value inscale
androtation
, an array is used to specify the exact values in each keyframe.
If no duration is specified, the frame will space the changes placed on the keyframes evenly.
Controlling animations
We've seen a lot of options on how to transition between animations, but there are some situations where you need to directly control when to start and/or end an animation. In those cases we can invoke a ready-to-use hook called useAnimation
.
As a simple example, let's say we want to do two animations, apart from the transition from hidden to visible, on the Play button intro screen:
// components/Intro
import { useEffect } from 'react'
import { motion, useAnimation } from 'framer-motion'
const buttonVariants = {
hidden: {
x: '500vw',
},
visible: {
x: 0,
transition: { type: 'spring', delay: 0.3, duration: 1 },
},
loop: {
scale: 1.5,
transition: {
duration: 0.4,
yoyo: Infinity,
},
},
}
const Intro = ({ next }: { next: () => void }) => {
const controls = useAnimation()
useEffect(() => {
const sequence = async () => {
await controls.start('visible')
return controls.start('loop')
}
sequence()
}, [controls])
return (
<div className="flex-vertical">
<h1>Memory Game</h1>
<motion.button
onClick={next}
variants={buttonVariants}
initial="hidden"
animate={controls}
>
Play
</motion.button>
</div>
)
}
export default Intro
- As you can see, after the transition from
hidden
tovisible
we want to do another animation, which in this case is an Infinity yo-yo animation, one of the solutions is to take the moment of the component's mount point withuseEffect
and perform the necessary actions. - The button now has
controls
as ananimate
value which is extracted from theuseAnimation
hook. - When the component is mounted, we can use
controls
to trigger any animation, which returns a promise that resolves when the animation ends.
Controls supports both the variants and the JS object we saw at the beginning of the article.
Exit animations
In addition to initial
and animate
there is a third state exit
that we can use to make animations when the component is removed from the DOM.
In this case, we want each game screen to exit the screen in the opposite direction it came from to give the feeling of sliding screens.
// components/Intro/
import { useEffect } from 'react'
import { motion, useAnimation } from 'framer-motion'
const containerVariants = {
exit: {
x: '-100vh',
transition: { ease: 'easeInOut' },
},
}
const Intro = ({ next }: { next: () => void }) => {
const controls = useAnimation()
useEffect(() => {
const sequence = async () => {
await controls.start('visible')
return controls.start('loop')
}
sequence()
}, [controls])
return (
<motion.div
className="flex-vertical"
variants={containerVariants}
exit="exit"
>
<h1>Memory Game</h1>
<button onClick={next}>Play</button>
</motion.div>
)
}
export default Intro
- In this case, we add an
exit
variant that moves the content to the left, away from the viewport.
If you try this code, it won't work, you will have to specify the parent element that needs to be aware of the presence of the components with AnimatePresence
. In this case, the parent component is the single page containing the whole game:
// pages/index.tsx
import { useState } from 'react'
import { AnimatePresence } from 'framer-motion'
import type { NextPage } from 'next'
import Game from '@/components/Game'
import Intro from '@/components/Intro'
import SelectDeck from '@/components/SelectDeck'
import SelectDifficulty, { Difficulties } from '@/components/SelectDifficulty'
import { Deck } from '@/types'
import { DECKS } from '@/utils/Decks'
const UIStates = {
IntroScreen: 0,
DifficultyScreen: 1,
DeckScreen: 2,
GameScreen: 3,
} as const
const Home: NextPage = () => {
const [UIState, setUIState] = useState<number>(UIStates.IntroScreen)
const [deck, setDeck] = useState<Deck>(DECKS['Dragon Ball'])
const [difficulty, setDifficulty] = useState(Difficulties.Normal)
return (
<div>
<AnimatePresence>
{UIState === UIStates.IntroScreen && (
<Intro next={() => setUIState(UIStates.DifficultyScreen)} />
)}
{UIState === UIStates.DifficultyScreen && (
<SelectDifficulty
next={() => setUIState(UIStates.DeckScreen)}
setDifficulty={setDifficulty}
/>
)}
{UIState === UIStates.DeckScreen && (
<SelectDeck
next={() => setUIState(UIStates.GameScreen)}
setDeck={(deckName: string) => setDeck(DECKS[deckName])}
/>
)}
{UIState === UIStates.GameScreen && (
<Game
selectedDeck={deck.slice(0, difficulty)}
backToDifficulty={() => setUIState(UIStates.DifficultyScreen)}
backToDeck={() => setUIState(UIStates.DeckScreen)}
/>
)}
</AnimatePresence>
</div>
)
}
export default Home
And I'm sorry to say that, despite adding AnimatePresence
, it still doesn't work! And that's because framer doesn't distinguish which component we are trying to animate when switching screens, so you need to specify an unique key for each screen.
{UIState === UIStates.IntroScreen && (
<Intro
next={() => setUIState(UIStates.DifficultyScreen)}
key={UIStates.IntroScreen}
/>
)}
Now it's working, but you'll see some weird animation where the first screen and the second screen exist at the same time. So, to fix that and the last step to get this animation working, is to tell framer that we want to delay the following animations until the exit animation is completely finished.
<AnimatePresence exitBefoeEnter>
Animations for SVG
A cool utility is the ability to animate the SVG, and it's as easy and simple as using pathLength
to animate the SVG path drawing process.
First, let's add this SVG to the introduction page:
// components/Intro/index.tsx
<svg
className={styles.Container}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<motion.path
fill="none"
stroke="var(--primary)"
strokeWidth={6}
strokeLinecap="round"
variants={pathVariants}
d="M256 224C238.4 224 223.1 238.4 223.1 256S238.4 288 256 288c17.63 0 32-14.38 32-32S273.6 224 256 224zM470.2 128c-10.88-19.5-40.51-50.75-116.3-41.88C332.4 34.88 299.6 0 256 0S179.6 34.88 158.1 86.12C82.34 77.38 52.71 108.5 41.83 128c-16.38 29.38-14.91 73.12 25.23 128c-40.13 54.88-41.61 98.63-25.23 128c29.13 52.38 101.6 43.63 116.3 41.88C179.6 477.1 212.4 512 256 512s76.39-34.88 97.9-86.13C368.5 427.6 441 436.4 470.2 384c16.38-29.38 14.91-73.13-25.23-128C485.1 201.1 486.5 157.4 470.2 128zM95.34 352c-4.001-7.25-.1251-24.75 15-48.25c6.876 6.5 14.13 12.87 21.88 19.12c1.625 13.75 4.001 27.13 6.751 40.13C114.3 363.9 99.09 358.6 95.34 352zM132.2 189.1C124.5 195.4 117.2 201.8 110.3 208.2C95.22 184.8 91.34 167.2 95.34 160c3.376-6.125 16.38-11.5 37.88-11.5c1.75 0 3.876 .375 5.751 .375C136.1 162.2 133.8 175.6 132.2 189.1zM256 64c9.502 0 22.25 13.5 33.88 37.25C278.6 105 267.4 109.3 256 114.1C244.6 109.3 233.4 105 222.1 101.2C233.7 77.5 246.5 64 256 64zM256 448c-9.502 0-22.25-13.5-33.88-37.25C233.4 407 244.6 402.7 256 397.9c11.38 4.875 22.63 9.135 33.88 12.89C278.3 434.5 265.5 448 256 448zM256 336c-44.13 0-80.02-35.88-80.02-80S211.9 176 256 176s80.02 35.88 80.02 80S300.1 336 256 336zM416.7 352c-3.626 6.625-19 11.88-43.63 11c2.751-12.1 5.126-26.38 6.751-40.13c7.752-6.25 15-12.63 21.88-19.12C416.8 327.2 420.7 344.8 416.7 352zM401.7 208.2c-6.876-6.5-14.13-12.87-21.88-19.12c-1.625-13.5-3.876-26.88-6.751-40.25c1.875 0 4.001-.375 5.751-.375c21.5 0 34.51 5.375 37.88 11.5C420.7 167.2 416.8 184.8 401.7 208.2z"
/>
</svg>
And the real magic behind it, the pathVariants
// components/Intro/Intro.variants.ts
const pathVariants = {
hidden: {
pathLength: 0,
},
visible: {
pathLength: 1,
transition: {
duration: 4,
yoyo: Infinity,
ease: 'easeInOut',
},
},
}
I've overcomplicated this with a bunch of additional properties that we already know about at this point but the key is to go from 0 pathLenght
to 1, framer motion will follow the path description of our SVG and draw that path with the animation values we specify.
Conclusion
With this simple project we have seen how easy, reliable and aligned with our current skills it is to include both simple and complex animations in our projects.
This is just an introductory guide to framer-motion, there is a lot more inside the library, especially a lot of utility hooks to make even crazier animations effortlessly and advanced topics like 3D animations by combining this library with react-three/fiber for example.
Be sure to check out the official documentation and try out different animations to take your projects to a new level.
Top comments (0)