Animated Hamburger Menu with react-spring
Resources
Step 1: Installation
npm install react-spring
# or
yarn add react-spring
Step 2: Header Component
Create a Header
component that will contain our state and animation.
We'll pass a function to the useSpring
hook and get back our animation styles
as well as an api
we can use for updating our animation values.
I abstracted some of the configuration values into animationConfig
to make it easier to tweak as we will reference these values in several places.
Read more about react-spring-config.
// Header.js
import React, { useState } from 'react'
import { useSpring } from 'react-spring'
const animationConfig = {
mass: 1,
frictionLight: 20,
frictionHeavy: 30,
tension: 575,
delay: 175,
}
const Header = () => {
const [open, toggle] = useState(false)
const [styles, api] = useSpring(() => ({
transformTop: 'translate(-6px, 10px) rotate(0deg)',
transformMiddle: 'translate(-6px, 0px) rotate(0deg)',
transformBottom: 'translate(-6px, -10px) rotate(0deg)',
widthTop: '24px',
widthBottom: '20px',
config: {
mass: animationConfig.mass,
friction: animationConfig.frictionHeavy,
tension: animationConfig.tension,
},
}))
return <header></header>
}
export default Header
Step 3: Creating Our Hamburger Menu
Create a HamburgerMenu
component and in it we'll need to import animated
from react-spring and extend each div
element we're animating.
// HamburgerMenu.js
import React from 'react'
import { animated } from 'react-spring'
const HamburgerMenu = () => {
return (
<button>
<animated.div />
<animated.div />
<animated.div />
</button>
)
}
export default HamburgerMenu
Give our button
an onClick
event.
We'll create a separate handleClick
function to handle our click event where we can use the start
method and update our animation based on the current open
state.
useSpring
allows us to chain animations in an array while being able to update our values and partially update config at each step.
For both opening and closing we're passing clamp: true
to the config in order to ease into the three bars coming together.
On the second half of our chained animations we pass clamp: false
which will allow our spring animation to be springy when animating to its final position.
We'll also add a slight delay to the start of the second half of our animations in order to let our menu lines rest overtop one another before springing out.
At the end of our handleClick
function we'll update the state with our toggle method.
Make sure to reference the correct styles for each of our animated.div
.
// HamburgerMenu.js
import React from 'react'
import { animated } from 'react-spring'
const HamburgerMenu = ({ open, toggle, api, styles, animationConfig }) => {
const handleClick = () => {
api.start({
to: open
? [
{
transformTop: 'translate(-6px, 18.5px) rotate(0deg)',
transformMiddle: 'translate(-6px, 0px) rotate(0deg)',
transformBottom:
'translate(-6px, -18.5px) rotate(0deg)',
widthTop: '28px',
widthBottom: '28px',
config: { clamp: true },
},
{
transformTop: 'translate(-6px, 10px) rotate(0deg)',
transformMiddle: 'translate(-6px, 0px) rotate(0deg)',
transformBottom:
'translate(-6px, -10px) rotate(0deg)',
widthTop: '24px',
widthBottom: '20px',
config: {
clamp: false,
friction: animationConfig.frictionLight,
tension: animationConfig.tension,
},
delay: animationConfig.delay,
},
]
: [
{
transformTop: 'translate(-6px, 18.5px) rotate(0deg)',
transformMiddle: 'translate(-6px, 0px) rotate(0deg)',
transformBottom:
'translate(-6px, -18.5px) rotate(0deg)',
widthTop: '28px',
widthBottom: '28px',
config: { clamp: true },
},
{
transformTop: 'translate(-6px, 18.5px) rotate(45deg)',
transformMiddle: 'translate(-6px, 0px) rotate(45deg)',
transformBottom:
'translate(-6px, -18.5px) rotate(-45deg)',
widthTop: '28px',
widthBottom: '28px',
config: {
clamp: false,
friction: animationConfig.frictionLight,
tension: animationConfig.tension,
},
delay: animationConfig.delay,
},
],
})
toggle((prev) => !prev)
}
return (
<button onClick={handleClick}>
<animated.div
style={{
transform: styles.transformTop,
width: styles.widthTop,
}}
/>
<animated.div
style={{
transform: styles.transformMiddle,
}}
/>
<animated.div
style={{
transform: styles.transformBottom,
width: styles.widthBottom,
}}
/>
</button>
)
}
export default HamburgerMenu
Step 4: Mobile Navigation
Lets go ahead and display our menu when the user clicks the hamburger button.
First create a new component called MobileNav
and import the necessary dependencies.
Here we're using useTransition
from react-spring
in order to animate our elements when they mount and unmount from the page.
I also went ahead and imported a couple icons from react-icons but its up to you what content you want to use.
When we return our UI from our component we pass a callBack to the transition function that contains our styles and our visible
item which is our open
state that we passed into the useTransition
hook.
Now when we click our hamburger menu open it will trigger our transition
.
open = true
- Mounts the page, starting at
from
styles and ending atenter
styles.
open = false
- Moving from
enter
styles toleave
styles and un-mounts the page.
// MobileNav.js
import { useEffect } from 'react'
import { useTransition, animated } from 'react-spring'
import { IoLogoInstagram, IoLogoGithub } from 'react-icons/io5'
const headings = ['Home', 'Blog', 'About', 'Contact']
const MobileNav = ({ open }) => {
useEffect(() => {
if (open) {
document.body.style.overflowY = 'hidden'
return
}
document.body.style.overflowY = 'auto'
}, [open])
const transition = useTransition(open, {
from: {
opacity: 0,
transformMain: 'translateY(40px)',
transformFoot: 'translateY(200px)',
},
enter: {
opacity: 1,
transformMain: 'translateY(0px)',
transformFoot: 'translateY(0px)',
},
leave: {
opacity: 0,
transformMain: 'translateY(40px)',
transformFoot: 'translateY(200px)',
},
})
return transition(({ opacity, transformMain, transformFoot }, visible) => {
return visible ? (
<animated.nav style={{ opacity }}>
<div>
<animated.ul style={{ transform: transformMain }}>
{headings.map((heading) => (
<li key={heading}>{heading}</li>
))}
</animated.ul>
<animated.div style={{ transform: transformFoot }}>
<IoLogoInstagram />
<IoLogoGithub />
</animated.div>
</div>
</animated.nav>
) : null
})
}
export default MobileNav
Rendering Our Components
Now we can render our HamburgerMenu
and MobileNav
components in our parent Header
component and pass each component the necessary props.
Render the MobileNav
outside the
element and wrap all our elements in a Fragment
so we don't cause the
element to grow to the full size of the page when our MobileNav
mounts on screen.
// Header.js
return (
<>
<header>
<HamburgerMenu
open={open}
toggle={toggle}
styles={styles}
api={api}
animationConfig={animationConfig}
/>
</header>
<MobileNav open={open} />
</>
)
Styling
We definitely need some styling so lets go ahead and do that by adding some classes to our elements and defining the styles.
// Header
return (
<>
<header className="header">
<HamburgerMenu
open={open}
toggle={toggle}
styles={styles}
api={api}
animationConfig={animationConfig}
/>
</header>
<MobileNav open={open} />
</>
)
// HamburgerMenu.js
return (
<button className="btn" onClick={handleClick}>
<animated.div
style={{ transform: styles.transformTop, width: styles.widthTop }}
className="menu-line"
/>
<animated.div
style={{ transform: styles.transformMiddle }}
className="menu-line"
/>
<animated.div
style={{
transform: styles.transformBottom,
width: styles.widthBottom,
}}
className="menu-line"
/>
</button>
)
// MobileNav.js
return visible ? (
<animated.nav style={{ opacity }} className="mobile-nav">
<div className="content-wrapper">
<animated.ul style={{ transform: transformMain }} className="list">
{headings.map((heading) => (
<li className="list-item" key={heading}>
{heading}
</li>
))}
</animated.ul>
<animated.div
className="icon-wrapper"
style={{ transform: transformFoot }}
>
<IoLogoInstagram className="icon" />
<IoLogoGithub className="icon" />
</animated.div>
</div>
</animated.nav>
) : null
/* styles.css */
.header {
position: relative;
display: flex;
justify-content: flex-end;
max-width: 48rem;
margin: 0 auto;
padding: 2rem 2rem 4rem;
z-index: 20;
}
.btn {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
width: 40px;
height: 40px;
padding: 0;
border: none;
overflow: hidden;
}
.menu-line {
height: 3px;
background-color: #000;
border-radius: 2px;
}
.menu-line:nth-of-type(2) {
width: 28px;
}
.mobile-nav {
position: fixed;
display: flex;
flex-direction: column;
box-sizing: border-box;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
width: 100%;
padding: 2rem 2rem 6rem 2rem;
z-index: 10;
}
.content-wrapper {
margin-top: 3rem;
}
.list {
list-style: none;
padding: 0;
}
.list-item {
font-size: 2.25rem;
font-weight: bold;
font-family: sans-serif;
margin: 1rem 0;
text-align: center;
}
.icon-wrapper {
display: flex;
justify-content: center;
margin-top: 3rem;
}
.icon {
width: 2.5rem;
height: 2.5rem;
}
.icon:nth-of-type(1) {
margin-right: 1.5rem;
}
Conclusion
In this article we walked through how to create a hamburger menu with react-spring. react-spring is one of my favorite animation libraries and can be used in so many interesting ways. I suggest you check out some of the examples shown in the react-spring documentation to get inspired.
Hopefully you found this helpful!