I’m of the idea that you can very easily distinguish good sites from expert sites.
The core difference is in the level of attention to detail.
To me, one of the things that subconsciously makes me go “Wow!” when I see a site are the subtle animations.
Take a look at stripe.com for example.
They have, in my opinion, one of the best landing pages of all tech companies. One key aspect of their landing page is the menu; I really like how it transitions from one item to the next.
In this guide, I’ll try to teach you how to level up your react animations game, so that the sites that you build also have that extra punch.
When you finish reading this guide you’ll know how to create animations like the following header component
Framer’s motion, our new best friend
After digging through many libraries, I’ve come to the conclusion that framer motion is the one that gives you the biggest bang for your buck.
It allows you to create animations, transitions, react to gestures, and more. They even provide you with a pretty well-featured “list reorder” component.
So let’s get started!
(you can check the GitHub repo with the code for this tutorial here: https://github.com/mikealche/animation-tutorial)
Level 1: Basic layout animations
In this first level, the idea is to get our feet wet and start playing around with animations and what one can do with them
To create our playground we will use Next.js the most popular react framework at the current time.
So let’s begin by installing what will be our toolbox for this guide:
- Next.js: our framework
- Framer Motion: the tool used to make animations
- Tailwind CSS: the CSS library that we’ll use
- Daisy UI: a tailwind CSS component library with nice defaults and excellent theming
Create the project and cd into it
yarn create next-app my-guide-to-animations && cd my-guide-to-animations
Now, let’s install everything that we listed above
yarn add framer-motion tailwindcss@latest postcss@latest autoprefixer@latest daisyui
Remember to complete the additional steps in the installation for tailwindcss AND for daisyUI. For daisy UI you will want to create a custom _document.js
file like it says here and add the data-theme='fantasy'
property (or any other) to the Html
tag that nextjs provides.
Now, let’s begin by creating just the top-level items of our menu.
import React from "react";
const NiceMenu = () => {
return (
<div className="w-screen p-20 ">
<div className="border p-10 flex justify-center">
<MenuItem text={"Home"}></MenuItem>
<MenuItem text={"About us"} style={{ minWidth: 400 }}></MenuItem>
<MenuItem text={"Products"} style={{ minWidth: 400 }}></MenuItem>
</div>
</div>
);
};
const MenuItem = ({ text, children }) => {
return (
<div className="px-10 relative cursor-pointer">
<span className="relative">{text}</span>
</div>
);
};
export default NiceMenu;
If you set up everything correctly you should see the following centered on the screen
Now, at this level, we will start by just adding the underline when we hover over each element.
However, we won’t be doing this with CSS (i.e: adding the hover:border-blue-300
class in tailwind).
We will instead use javascript to monitor whether an element is being hovered, and based on that, display the underline (which will be a new JSX component)
Explaining why we will be doing this in sort of more complicated allows me to introduce the first topic in Framer’s Motion: layout animations:
In Framer, if you mark two distinct components with the same layoutId
property and you only show one at a time, when you toggle between them framer will automagically create the animation from one to the other.
Did you catch that? Let me rephrase it, just by magically adding the layoutId
property to two completely different elements we can animate between them!
In our case, we will create a new JSX Component called Underline
which will be basically a div
of small height and a nice gradient background-color.
Then under each MenuItem
we will conditionally render an instance of Underline
if the MenuItem
is being hovered.
Before I show you the code, here are some details that you also need to know:
- Apart from giving the
Underline
component a layoutId, we also need to set the layout property to enable layout animations - For div’s to be able to accept these properties, we must add the
motion
library fromframer-motion
and usemotion.div
instead
So here’s the code for the basic underline that “runs” from one menu item to the other:
import { motion } from "framer-motion";
import React, { useState } from "react";
const Underline = () => (
<motion.div
className="absolute -bottom-1 left-0 right-0 h-1 bg-gradient-to-r from-blue-700 via-pink-500 to-red-500"
layoutId="underline"
layout
></motion.div>
);
const NiceMenu = () => {
return (
<div className="w-screen p-20 ">
<motion.div className="border p-10 flex justify-center">
<MenuItem text={"Home"}></MenuItem>
<MenuItem text={"About us"} style={{ minWidth: 400 }}></MenuItem>
<MenuItem text={"Products"} style={{ minWidth: 400 }}></MenuItem>
</motion.div>
</div>
);
};
const MenuItem = ({ text, children, ...props }) => {
const [isBeingHovered, setIsBeingHovered] = useState(false);
return (
<motion.div
className="px-10 relative cursor-pointer"
onHoverStart={() => setIsBeingHovered(true)}
onHoverEnd={() => setIsBeingHovered(false)}
>
<span className="relative">
{text}
{isBeingHovered && <Underline />}
</span>
</motion.div>
);
};
export default NiceMenu;
That code produces the following output:
Just a few changes here and there and Framer already gives us something to play with.
Let’s keep going!
Level 2: Even more layout animations
To reinforce the concept of layout animations —given how powerful they are— we will now create another layout animation between the boxes that contain the SubItem
‘s of each header.
We will:
- Add to each
MenuItem
. a container for itsSubItem
‘s calledSubItemsContainer
. - We will conditionally render each
SubItemsContainer
based on whether the topMenuItem
is being hovered - Each
SubItemsContainer
will share the samelayoutId
but it will be different than thelayoutId
prop we passed toUnderline
- To make everything a bit nicer, we will add the
HashIcon
library which will automatically generate a nice Icon for eachSubItem
element - Remember to wrap the
SubItemsContainer
into its ownmotion.div
component
This gives us the following code:
import { motion } from "framer-motion";
import React, { useState } from "react";
import { Hashicon } from "@emeraldpay/hashicon-react";
const Underline = () => (
<motion.div
className="absolute -bottom-1 left-0 right-0 h-1 bg-gradient-to-r from-blue-700 via-pink-500 to-red-500"
layoutId="underline"
layout
></motion.div>
);
const NiceMenu = () => {
return (
<div className="w-screen p-20">
<motion.div className="border p-10 flex justify-center">
<MenuItem text={"Home"}>
<SubItem title="Product" text="A SaaS for e-commerce" />
<SubItem title="Blog" text="Latest posts" />
<SubItem title="Contact" text="Get in touch" />
</MenuItem>
<MenuItem text={"About us"} style={{ minWidth: 400 }}>
<SubItem title="The Team" text="Get to know us better" />
<SubItem title="The Company" text="Since 1998" />
<SubItem
title="Our Mission"
text="Increase the GDP of the internet"
/>
<SubItem title="Investors" text="who's backing us" />
</MenuItem>
<MenuItem text={"Products"} style={{ minWidth: 400 }}>
<SubItem
title="Ecommerce"
text="Unify online and in-person payments"
/>
<SubItem
title="Marketplaces"
text="Pay out globally and facilitate multiparty payments"
/>
<SubItem
title="Platforms"
text="Let customers accept payments within your platform"
/>
<SubItem
title="Creator Economy"
text="Facilitate on-platform payments and pay creators globally"
/>
</MenuItem>
</motion.div>
</div>
);
};
const MenuItem = ({ text, children }) => {
const [isBeingHovered, setIsBeingHovered] = useState(false);
return (
<motion.div
className="px-10 relative cursor-pointer"
onHoverStart={() => setIsBeingHovered(true)}
onHoverEnd={() => setIsBeingHovered(false)}
>
<span className="relative">
{text}
{isBeingHovered && <Underline />}
</span>
{isBeingHovered && <SubItemsContainer>{children}</SubItemsContainer>}
</motion.div>
);
};
const SubItemsContainer = ({ children }) => {
return (
<div className="py-5 min-w-max">
<motion.div
layoutId="menu"
className="absolute border border-1 shadow-lg py-10 px-10 bg-white rounded-box -left-2/4"
style={{ minWidth: 400 }}
initial="hidden"
animate="visible"
>
{children}
</motion.div>
</div>
);
};
const SubItem = ({ title, text }) => {
return (
<div className="my-2 group cursor-pointer min-w-max">
<div className="flex items-center gap-4">
<Hashicon value={title} size={25} />
<div className="">
<p className="font-bold text-gray-800 group-hover:text-blue-900 text-md">
{title}
</p>
<span className="font-bold text-gray-400 group-hover:text-blue-400 text-sm">
{text}
</span>
</div>
</div>
</div>
);
};
export default NiceMenu;
And now, things should start looking pretty decent:
There are some problems though
If you look closely enough, you’ll see there are some problems with the SubItems'
text getting stretched out and then shrunk.
This happens because child components that are being part of a layout animation might suffer distortions if we don’t mark them with the layout
property.
If we now change the SubItems
to also be a motion.div
and make them use the layout
property, the stretching will be gone.
Replace the SubItem
JSX component for the following code
const SubItem = ({ title, text }) => {
return (
<motion.div className="my-2 group cursor-pointer min-w-max" layout>
<div className="flex items-center gap-4">
<Hashicon value={title} size={25} />
<div className="">
<p className="font-bold text-gray-800 group-hover:text-blue-900 text-md">
{title}
</p>
<span className="font-bold text-gray-400 group-hover:text-blue-400 text-sm">
{text}
</span>
</div>
</div>
</motion.div>
);
};
Phew, avoided one problem!
But hey, now our SubItem
‘s are motion.div
what do you say if we go ahead and animate them as well?
Level 3: Variants and child animations
When you go to read Framer’s Motion documentation site, the first things that they teach you are using the animate
property and how to animate using variants
.
I decided to invert the order and talk about layout animations first because, in my opinion, they are the easiest and give you the biggest bang for your buck.
But now, we will follow the book and talk about the animate property and how to use what they call variants
.
The ‘animate’ property
Framer motion 101 as short as possible:
- A component can have two properties:
initial
andanimate
initial
describes the styles of the component “at the start” of the animationanimate
describes the styles of the component “at the end” of the animation- Framer motion will interpolate between initial and animate, creating an animation
In reality, things are a bit harder than that, but for now, that should suffice. To dive deeper you can always read the official docs.
To give an easy example, suppose the following code
<motion.div
initial={{ height: 0, width: 0, backgroundColor: "#0000ff" }}
animate={{ height: 200, width: 200, backgroundColor: "#c00030" }}
/>
Can you guess what that will animate to?
It will animate to an expanding square that changes colors from blue to red 🙂
If you guessed somewhat similar, let’s keep going, if not, try reading the docs a bit, or playing around with the code to understand it better.
Variants
The name “Variants” already sounds like we’re going to jump into a pool of complexity.
Well, we’re going to, sort of… but not that much!
Using variants is basically another way to use the properties initial
and animate
, that comes with some additional benefits.
To use variants you just need to extract the objects that you passed into initial
and animate
, and make them properties of a brand new object.
In the previous section, we had
<motion.div
initial={{ height: 0, width: 0, backgroundColor: "#0000ff" }}
animate={{ height: 200, width: 200, backgroundColor: "#c00030" }}
/>
So now we:
- extract the objects into properties of a brand new object.
- pass this object into our square as the
variants
prop - pass the name of the key of the initial state to the
initial
prop, and pass the name of the key of the end state to theanimate
property
const myAnimatedSquareVariants = {
howItShouldLookLikeAtTheStart: {
height: 0,
width: 0,
backgroundColor: "#0000ff",
},
howItShouldLookLikeAtTheEnd: {
height: 200,
width: 200,
backgroundColor: "#c00030",
},
};
<motion.div
initial="howItShouldLookLikeAtTheStart"
animate="howItShouldLookLikeAtTheEnd"
variants={myAnimatedSquareVariants}
/>
I’m being over-explicity on purpose on the object names for you to realize that you can name the object keys as you’d like.
There are no hidden keywords that we’re targeting nor anything weird that you may imagine.
The benefits of using variants
OK, so now that we’ve done the hard part, let’s reap the benefits!
When we pass strings to the initial
and animate
properties of a component, every child object of that component will also try to search to see if it has variants whose name matches those passed to its parent component.
An example is worth more than a thousand words:
import { motion } from "framer-motion";
const myAnimatedSquareVariants = {
howItShouldLookLikeAtTheStart: {
height: 0,
width: 0,
backgroundColor: "#0000ff",
},
howItShouldLookLikeAtTheEnd: {
height: 200,
width: 200,
backgroundColor: "#c00030",
transition: {
delayChildren: 0.5,
},
},
};
const myAnimatedText = {
howItShouldLookLikeAtTheStart: {
opacity: 0,
},
howItShouldLookLikeAtTheEnd: {
opacity: 1,
color: "#fff",
scale: 3,
},
};
<motion.div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
initial="howItShouldLookLikeAtTheStart"
animate="howItShouldLookLikeAtTheEnd"
variants={myAnimatedSquareVariants}
>
<motion.h1 variants={myAnimatedText}>Hey</motion.h1>
</motion.div>;
The above code will result in the following:
As you can see, the motion.h1
doesn’t need to specify its own initial
nor animate
properties.
Just by:
- Being a child of the parent
motion.div
component (which does specify those props) - Providing its
variants
prop with an object with the same keys that the parent specifies as theinitial
andanimate
keys
We get the children animated as well!
Applying this to our menu
If we were to apply this to our menu we would get the following:
import { motion } from "framer-motion";
import React, { useState } from "react";
import { Hashicon } from "@emeraldpay/hashicon-react";
const Underline = () => (
<motion.div
className="absolute -bottom-1 left-0 right-0 h-1 bg-gradient-to-r from-blue-700 via-pink-500 to-red-500"
layoutId="underline"
layout
></motion.div>
);
const NiceMenu = () => {
return (
<div className="w-screen p-20">
<motion.div className="border p-10 flex justify-center">
<MenuItem text={"Home"}>
<SubItem title="Product" text="A SaaS for e-commerce" />
<SubItem title="Blog" text="Latest posts" />
<SubItem title="Contact" text="Get in touch" />
</MenuItem>
<MenuItem text={"About us"} style={{ minWidth: 400 }}>
<SubItem title="The Team" text="Get to know us better" />
<SubItem title="The Company" text="Since 1998" />
<SubItem
title="Our Mission"
text="Increase the GDP of the internet"
/>
<SubItem title="Investors" text="who's backing us" />
</MenuItem>
<MenuItem text={"Products"} style={{ minWidth: 400 }}>
<SubItem
title="Ecommerce"
text="Unify online and in-person payments"
/>
<SubItem
title="Marketplaces"
text="Pay out globally and facilitate multiparty payments"
/>
<SubItem
title="Platforms"
text="Let customers accept payments within your platform"
/>
<SubItem
title="Creator Economy"
text="Facilitate on-platform payments and pay creators globally"
/>
</MenuItem>
</motion.div>
</div>
);
};
const MenuItemVariants = {
hidden: {
opacity: 0,
},
visible: {
x: 0,
opacity: 1,
},
};
const MenuItem = ({ text, children, ...props }) => {
const [isBeingHovered, setIsBeingHovered] = useState(false);
return (
<motion.div
className="px-10 relative cursor-pointer"
onHoverStart={() => setIsBeingHovered(true)}
onHoverEnd={() => setIsBeingHovered(false)}
>
<span className="relative">
{text}
{isBeingHovered && <Underline />}
</span>
{isBeingHovered && (
<div className="py-5 min-w-max ">
<motion.div
{...props}
layoutId="menu"
className="absolute border border-1 shadow-lg py-10 px-10 bg-white rounded-box -left-2/4"
variants={MenuItemVariants}
style={{ minWidth: 400 }}
initial="hidden"
animate="visible"
>
{children}
</motion.div>
</div>
)}
</motion.div>
);
};
const SubItemVariants = {
hidden: {
x: -20,
opacity: 0,
},
visible: {
x: 0,
opacity: 1,
},
};
const SubItem = ({ title, text }) => {
return (
<motion.div
className="my-2 group cursor-pointer min-w-max"
layout
variants={SubItemVariants}
>
<div className="flex items-center gap-4">
<Hashicon value={title} size={25} />
<div className="">
<p className="font-bold text-gray-800 group-hover:text-blue-900 text-md">
{title}
</p>
<span className="font-bold text-gray-400 group-hover:text-blue-400 text-sm">
{text}
</span>
</div>
</div>
</motion.div>
);
};
export default NiceMenu;
As you can see, we now are defining the MenuItemVariants
and the SubItemVariants
objects which share the same key names. And we’re only specifying the animate
and initial
properties inside the MenuItem
component.
This should give us the following output:
Looking pretty decent now with the subitems being animated as well.
Let’s finish this up on the next level
Level 4: Transition properties
I really like this level because it will let us achieve a lot, without doing too much.
In the previous level, we talked about the variants
objects on which each key
would contain an object with some styles for a component.
const MenuItemVariants = {
hidden: {
opacity: 0,
},
visible: {
x: 0,
opacity: 1,
},
};
The thing is, this object
, apart from style properties, can contain a transition
property.
The values that that transition
property can have are many and very powerful. To see a complete list of all the things that can be specified you can, again, check the docs.
What we want to do right now is that every subitem of a menu shows up with a little bit more delay than the previous item.
This is so common, that framer motion has a specific property for that: staggerChildren
Let’s replace our MenuItemVariants
with that
const MenuItemVariants = {
hidden: {
opacity: 0,
},
visible: {
x: 0,
opacity: 1,
transition: {
staggerChildren: 0.05,
},
},
};
If we now piece everything together, we will get the final output that I showed at the beginning!
Congrats! You now know enough react animations to start being dangerous!
If you want to keep learning this kind of content on react animations you can subscribe to the newsletter and follow me on Twitter.
If you’d like to hire me as a contractor for your project, send me a message.
Best regards 🙂
Mike – React Software Consulting