Learn React Animations by Creating a Stripe inspired Menu

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 from framer-motion and use motion.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 its SubItem‘s called SubItemsContainer.
  • We will conditionally render each SubItemsContainer based on whether the top MenuItem is being hovered
  • Each SubItemsContainer will share the same layoutId but it will be different than the layoutId prop we passed to Underline
  • To make everything a bit nicer, we will add the HashIcon library which will automatically generate a nice Icon for each SubItem element
  • Remember to wrap the SubItemsContainer into its own motion.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 and animate
  • initial describes the styles of the component “at the start” of the animation
  • animate 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 🙂

animated square 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 the animate 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:

  1. Being a child of the parent motion.div component (which does specify those props)
  2. Providing its variants prop with an object with the same keys that the parent specifies as the initial and animate 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

Leave a Comment

Your email address will not be published. Required fields are marked *