How to Create A Sliding Sidebar Menu with Framer Motion

Will Johnson
author
Will Johnson
Glowing ring with adjacent pillars reflecting on a smooth surface.

Animations do more than just look good. They can draw the user's attention to important information or let them know they can interact with certain elements.

Everything we do in the real world has motion. When you turn on the faucet to get a glass of water, you see the water stream into the glass and fill up. It would be weird if the water just teleported into the glass.

We do have a lot of motion on mobile apps, and for whatever reason, we've lost that on the web. Framer Motion is a React animation library that makes it simple and declarative to add animations to your React apps without being an animation expert.

What You Need To Know

This blog post will teach you a way to create a sliding navigation menu with Framer Motion. This post assumes you understand React features like JSX, functional components, and hooks.

You are not expected to know anything about animating on the web or Framer Motion. All concepts will be explained as you come across them, and links to the docs will be provided.

Getting Started

You'll start with a styled React app and get right into adding Framer Motion.

Here is the starting code:

App.js

import "./styles.css";
import React from "react";
const links = [
{ name: "Home", to: "#", id: 1 },
{ name: "About", to: "#", id: 2 },
{ name: "Blog", to: "#", id: 3 },
{ name: "Contact", to: "#", id: 4 }
];
export default function App() {
return (
<main>
<aside>
<div className="container">
{links.map(({ name, to, id }) => (
<a key={id} href={to}>
{name}
</a>
))}
</div>
</aside>
<div className="btn-container">
<button>Close</button>
</div>
</main>
);
}

Follow along with this tutorial using the demo CodeSandbox with Framer Motion already added here

Install Framer Motion

If you haven't already, the first thing you want to do is install Framer Motion into your project.

npm install framer-motion

Now that you have Framer Motion installed, import motion from Framer Motion.

App.js

import { motion } from "framer-motion"

Create a Motion Component and Start Animating

Motion components are what bring the magic of Framer Motion into your application. Add motion. as a prefix to any HTML or SVG opening and closing tag to make it a motion component. We'll start with the <aside> element in this example.

App.js

<motion.aside>
<div className="container">
{links.map(({ name, to, id }) => (
<a key={id} href={to}>
{name}
</a>
))}
</div>
</motion.aside>

<aside> is now a motion component that gets access to special props from Framer Motion that let you declaratively create your animations.

The first prop you'll add is initial. This lets you set the state you want the element to be in on page load. The initial prop takes an object with a key-value pair that says what property you want to change and how you want it to change.

Add the initial prop to <aside>.

App.js

<motion.aside initial={{ width: 0 }}>
<div className="container">
{links.map(({ name, to, id }) => (
<a key={id} href={to}>
{name}
</a>
))}
</div>
</motion.aside>

Passing the initial prop an object with a key of width and a value of 0 tells Framer you want the width of the aside to be zero pixels on page load

Another prop you can use is animate. This is is the state you want the animation to end at.

In this example, set the width of the aside to 300

App.js

<motion.aside
initial={{ width: 0 }}
animate={{ width: 300 }}
>
<div className="container">
{links.map(({ name, to, id }) => (
<a key={id} href={to}>
{name}
</a>
))}
</div>
</motion.aside>

NOTE: Notice you only typed 300 not 300px. Framer automatically chooses px for you. To use another CSS unit like vw or em wrap the value in double quotes. For example - animate={{ width: "30vw" }}

Now refresh the page and see the aside slide in 300 pixels.

Create Variants to Animate Children

Now you are getting into some fun with the variants prop. Variants let you define your animation states outside of your element and reference the animation by any name you give.

The coolest part about variants is that when a parent element has the variants prop, any child element in the React tree gets the animate prop for free added behind the scenes, and you can have animations flow down the tree! Start by creating variants object for the aside element, with object keys aka variants labels for the states of the animation with values that are also objects.

App.js

const sideVariants = {
closed: {
},
open: {
}
};

NOTE: You can name variants and the variant labels whatever you want

Leave sideVariants empty for now. Create another variant object for items from the links array. With variant labels of open and closed. For closed for use the value of {opacity : 0 }, and { opacity: 1 } for open.

App.js

const itemVariants = {
closed: {
opacity: 0
},
open: { opacity: 1 }
};

When the link elements are in the closed state, they will be invisible, and when they are in the open state, they will be completely visible.

You can more about CSS opacity here

Back in the sideVariants, add a transition object as the value. Transition is another prop that you can use in motion components, and it tells Framer Motion how you want the animation to go from one animation state to the next.

Now use the staggerChildren. Since the links are wrapped in a div and therefore are its children, staggerChildren will animate each child item. You can set amount of time in between each animation as well!

For example, if staggerChildren is 0.01, the first child will be delayed by 0 seconds, the second by 0.01, the third by 0.02, and so on.

You can also change which order they will animate with staggerDirection. 1 staggers from the first to the last. -1 staggers in reverse from the last to the first. We'll use this later when the elements get removed from the DOM.

App.js

const sideVariants = {
closed: {
transition: {
staggerChildren: 0.2,
staggerDirection: -1
}
},
open: {
transition: {
staggerChildren: 0.2,
staggerDirection: 1
}
}
};

Add Variants to Motion Components

Now that we have our variants created. Add them to the motion components. Convert the wrapper div and the a tags to motion components.

<motion.aside initial={{ width: 0 }} animate={{ width: 300 }}>
<motion.div className="container">
{links.map(({ name, to, id }) => (
<motion.a key={id} href={to}>
{name}
</motion.a>
))}
</motion.div>
</motion.aside>

Next, use the animations defined in the variant labels by referencing them in the motion component props. Add the initial, animate, and variants props to the container div. Set initial and to a string of "closed", animate to "open" and variants to the sideVariants object.

App.js

<motion.aside initial={{ width: 0 }} animate={{ width: 300 }}>
<motion.div
className="container"
initial="closed"
animate="open"
variants={sideVariants}
>
{links.map(({ name, to, id }) => (
<motion.a key={id} href={to}>
{name}
</motion.a>
))}
</motion.div>
</motion.aside>

Now, you can add the variants prop to the motion.a tag and set it to itemVariants.

App.js

<motion.div
className="container"
initial="closed"
animate="open"
variants={sideVariants}
>
{links.map(({ name, to, id }) => (
<motion.a key={id} href={to} variants={itemVariants}>
{name}
</motion.a>
))}
</motion.div>

When you refresh the page, the links should animate and become visible one by one, aka staggered. Notice how in order to animate the motion.a tag, you didn't need to add the animate prop to make the elements animate. The container div made it possible for all its children to be animated when we added the variants prop.

Add useCycle hook to Switch Between Animation States

The useCycle hook rotates or "cycles" through a set of animation states, and it works like the beloved useState. You provide useCycle an initial array of states, and it returns an array of two arguments.

You'll set the cycle variable to open and cycle between the true or false with a cycle function.

Start by importing useCycle from Framer Motion. Then destruct open and cycleOpen from useCycle just like you would with useState. Pass false and true as arguments to useCycle.

App.js

import { motion, useCycle } from "framer-motion";
export default function App() {
const [open, cycleOpen] = useCycle(false, true);
return (
....

Next, you want to conditionally render the motion.aside based on if the value of open is true or false.

App.js

<main>
{open && (
</motion.aside>
.....
</motion.aside>
)}

Finally, you want to add the onClick event handler to the button, pass the cycleOpen function. Then, add a ternary operator that changes the button's text to display open or closed depending on the value of open.

App.js

<div className="btn-container">
<button onClick={cycleOpen}>{open ? "Close" : "Open"}</button>
</div>

Animate Components Removed from the React Tree with AnimatePresence

Right now, the aside just pops out of vision when you click Close. That's a no-no for the app you're building. It's too stark, and the user loses the context of what is happening.

Framer Motion has the AnimatePresence component that allows components to be animated out when removed from the React tree.

You need AnimatePresence to activate exit animations because React doesn't have a lifecycle method that lets components know when they're going to be unmounted and allows them to delay the unmount until after an animation is over.

First, import AnimatePresence

import { AnimatePresence, motion, useCycle } from "framer-motion";

Next, wrap the aside motion component with AnimatePresence.

App.js

<AnimatePresence>
{open && (
<motion.aside
initial={{ width: 0 }}
animate={{
width: 300
}}
>
<motion.div
className="container"
initial="closed"
animate="open"
exit="closed"
variants={sideVariants}
>
{links.map(({ name, to, id }) => (
<motion.a
key={id}
href={to}
whileHover={{ scale: 1.1 }}
variants={itemVariants}
>
{name}
</motion.a>
))}
</motion.div>
</motion.aside>
)}
</AnimatePresence>

Now that aside is wrapped in AnimatePresence, it has access to the exit prop. The exit prop is where you define how you want the element to animate when it leaves the React tree.

Here you'll pass the exit prop this object. {width: 0, transition: { delay: 0.7, duration: 0.3 }}.

App.js

<AnimatePresence>
{open && (
<motion.aside
initial={{ width: 0 }}
animate={{
width: 300
}}
exit={{
width: 0,
transition: { delay: 0.7, duration: 0.3 }
}}
>
.....
</AnimatePresence>

With Framer Motion, you don't have to specify that you're using seconds for how long you want the animations to last since Framer defaults to seconds

Also, add an exit prop to the container div around the motion.a components. And set its animation to the variant label " closed"

App.js

<motion.div
className="container"
initial="closed"
animate="open"
exit="closed"
variants={sideVariants}
>
<motion.a
key={id}
href={to}
whileHover={{ scale: 1.1 }}
variants={itemVariants}
>
{name}
</motion.a>
))}
</motion.div>

This animates the aside and the a tags when removed from the DOM after the "Close" button is clicked.

Add Gestures to show Interactivity

The last thing you do is add a hover animation to the links in the menu and a subtle animation to let the user know that this element can be interacted with.

Framer Motion adds event listeners to React with gesture helper props. These props let you create animations based on different interactions with elements. Some of the gestures props are whileHover, whileTap, whileFocus, and more.

In the example, you'll add a hover animtion. Add the whileHover prop to the motion.a component, and pass in an object with scale: 1.1 as the key-value pair.

App.js

<motion.a
key={id}
href={to}
whileHover={{ scale: 1.1 }}
variants={itemVariants}
>
{name}
</motion.a>

Notice how you didn't have to create every detail of the animation. You just told Framer what you wanted to happen, and Framer Motion took care of the details.

Here is the final code

import "./styles.css";
import React from "react";
import { AnimatePresence, motion, useCycle } from "framer-motion";
const links = [
{ name: "Home", to: "#", id: 1 },
{ name: "About", to: "#", id: 2 },
{ name: "Blog", to: "#", id: 3 },
{ name: "Contact", to: "#", id: 4 }
];
const itemVariants = {
closed: {
opacity: 0
},
open: { opacity: 1 }
};
const sideVariants = {
closed: {
transition: {
staggerChildren: 0.2,
staggerDirection: -1
}
},
open: {
transition: {
staggerChildren: 0.2,
staggerDirection: 1
}
}
};
export default function App() {
const [open, cycleOpen] = useCycle(false, true);
return (
<main>
<AnimatePresence>
{open && (
<motion.aside
initial={{ width: 0 }}
animate={{
width: 300
}}
exit={{
width: 0,
transition: { delay: 0.7, duration: 0.3 }
}}
>
<motion.div
className="container"
initial="closed"
animate="open"
exit="closed"
variants={sideVariants}
>
{links.map(({ name, to, id }) => (
<motion.a
key={id}
href={to}
whileHover={{ scale: 1.1 }}
variants={itemVariants}
>
{name}
</motion.a>
))}
</motion.div>
</motion.aside>
)}
</AnimatePresence>
<div className="btn-container">
<button onClick={cycleOpen}>{open ? "Close" : "Open"}</button>
</div>
</main>
);
}

And here is the link to the finished code in a code sandbox

Conclusion

Now you have an animated sidebar that animates on a button press.

You learned how to use Framer Motions features like Gestures to animate on hover, Animate Presence to animate when an item leaves the DOM, and the useCycle hook to cycle through different animation states.

Resources