Go back
Go back

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 at enter styles.

open = false

  • Moving from enter styles to leave 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

mobile menu animation preview

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!