How to Make a Responsive, Animated Navbar in React

How to Make a Responsive, Animated Navbar in React

With dropdown functionality
Ferenc Almasi β€’ 2023 February 23 β€’ Read time 18 min read β€’ React v18.2
Learn how you can create a responsive animated navbar component in React with the help of CSS media queries and transitions.
  • twitter
  • facebook
React Previous Tutorial

Creating a responsive, animated navigation bar with dropdown functionality in React can be a tricky task. There are many subtle elements involved, starting from how to semantically generate the HTML to how to style and animate elements in CSS.

Animated responsive navbar in React
The output of this tutorial

In this tutorial, we are going to go through step-by-step how to create a Navigation component, as well as how to animate each part. At the end of this tutorial, we are going to have the above navbar ready.


Setting Up the Component

Starting off, we want to define the data structure of our navbar. In a real-world scenario, we would likely get the data from a CMS. In this tutorial, we are going to define it as a variable. Inside the App/Layout component, add the following array of objects:

Copied to clipboard! Playground
const items = [
    { name: 'Home', url: '/' },
    {
        name: 'Tutorials',
        children: [
            { name: 'Beginner', url: '/tutorials/beginner' },
            { name: 'Intermediate', url: '/tutorials/intermediate' },
            { name: 'Advanced', url: '/tutorials/advanced' }
        ]
    },
    { ... },
    { ... }
]
App.jsx
Defining the structure of menu

Each item comes with the following properties:

  • name: The name of the menu.
  • url: The URL where the menu is pointing to. If this is not set, it means the menu item is a dropdown.
  • children: If the menu item is a dropdown, it will have a list of child elements, each with a name and url property.

We can pass this data to a new component called Navigation. Create an empty file for the component and reference it inside App.jsx:

Copied to clipboard!
<Navigation items={items} />
πŸ” Login to get access to the full source code in one piece. TypeScript types and React Router are also included.

Generating the Menu

Inside the component, the following elements will be necessary to handle the functionality on both mobile and desktop:

Copied to clipboard! Playground
const Navigation = ({ items }) => {
    return (
        <nav>
            <div className="container">
                <div className="logo" />
                <div className="hamburger">
                    <span className="meat"></span>
                    <span className="meat"></span>
                    <span className="meat"></span>
                    <span className="meat"></span>
                </div>
            </div>
            <ul className="menu">{renderItems()}</ul>
        </nav>
    )
}
Navigation.jsx
Create the layout in the component
  • nav: Everything will go inside a nav element, which is going to be displayed as flex. To have the correct alignment, we need to wrap the .logo and .hamburger elements into a .container.
  • .hamburger: This will be responsible for toggling the menu on mobile. Based on its design, it is often called a "hamburger" menu.
  • ul: This is where the navigation element will go. To keep indentation low, we can outsource the rendering into a function called renderItems.
Alignment of DOM elements
How the elements are laid out

Rendering navigation items

Copied to clipboard! Playground
const renderItems = () => items.map((item, index) => (
    <li key={index}>
        {item.url
            ? <Link to={item.url}>{item.name}</Link>
            : <span>
                  {item.name}
                  <img src="/arrow.svg" />
              </span>
        }
        {item.children && renderChildren(item.children)}
    </li>
))
Navigation.jsx
Define renderItems above the return statement

Based on whether the item has a url, we want to either render a Link pointing to item.url, or a span without a link, and an arrow pointing down. This arrow will only be visible on mobile, indicating that the menu item is collapsible.

This tutorial uses react-router for navigation.

Again, to keep the indentation flat, we can outsource the rendering of the dropdown to another function. Define renderChildren next to renderItems based on the code below:

Copied to clipboard! Playground
const renderChildren = (children) => (
    <ul className="sub-menu">
        {children.map((child, index) => (
            <li key={index}>
                <Link to={child.url}>
                    {child.name}
                </Link>
            </li>
        ))}
    </ul>
)
Navigation.jsx
renderChildren will render the dropdown menus

The logic roughly remains the same, but this time, every item is a link. Notice that we also want to wrap everything inside a ul, and don't forget to add the key attributes to the li elements.

Looking to improve your skills? Check out our interactive course to master React from start to finish.
Master Reactinfo Remove ads

Styling the Mobile Navigation

So far, we have generated the navbar with navigation functionality. However, we are missing the biggest piece from the component: the styles. Create a new CSS file for the navigation and import it into the component.

First, we want to reset some styles and give a proper background to our navigation. To do this, add the following styles to the empty CSS file:

Copied to clipboard!
nav {
    background: rgb(27,149,237);
    background: radial-gradient(ellipse at center bottom, rgba(27,149,237,1) 0%, rgba(27,87,153,1) 100%);
    display: flex;
    flex-direction: column;
}

a {
    color: #FFF;
    text-decoration: none;
}

ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
}
navigation.scss
Reset the above styles in CSS

This tutorial uses Sass for nesting styles. If you are using Vite, run npm i sass in the terminal to add it.

We can position radial gradients in CSS by defining the position after the ellipse keyword. It also accepts percentages, such as: ellipse at 50% 50%. To use the mobile-first approach, we are using flex-direction: column for the nav. This will be row for the desktop version.

mobile vs desktop layout
Mobile vs desktop layout

Styling the hamburger menu

Next up, we want to style the hamburger menu. Visually, we have three bars, however, we have four .meat elements in the DOM. This is because the fourth is hidden behind the middle bar.

hamburger menu animation
Menu animation in slow motion

This will be animated into a cross with rotation, while also hiding the top and bottom bars simultaneously. To align the bars correctly, add the following rules to the CSS:

Copied to clipboard!
.hamburger {
    position: relative;
    width: 30px;
    height: 20px;
    cursor: pointer;
    user-select: none;

    .meat {
        border-radius: 2px;
        width: 100%;
        position: absolute;
        height: 3px;
        background: #FFF;
        display: block;
        transition: all .3s cubic-bezier(0.4, 0.0, 0.2, 1);

        &:first-child {
            top: 0;
        }

        &:nth-child(2),
        &:nth-child(3) {
            top: 50%;
            transform: translateY(-50%);
        }

        &:last-child {
            bottom: 0;
        }
    }
}
navigation.scss
Aligning the bars in the hamburger menu

The important parts here are the highlighted lines. We can use absolute positioning to align the elements at the top (first-child), middle, and bottom (last-child). Notice that the second and third children are in the same position. These elements will be turned into a cross. We can animate them with an additional class:

Copied to clipboard!
.close {
    .meat:first-child,
    .meat:last-child {
        opacity: 0;
    }

    .meat:first-child {
        transform: translateY(20px) scale(0);
    }

    .meat:last-child {
        transform: translateY(-20px) scale(0);
    }

    .meat:nth-child(2) {
        transform: rotate(45deg);
    }

    .meat:nth-child(3) {
        transform: rotate(-45deg);
    }
}
navigation.scss
Animating the hamburger into a cross
  • The first and last children are animated into the middle, while also making it invisible with scaling and opacity.
  • The second and third children are rotated by (+/-)45Β° to form a cross.

This class can be toggled through an onClick event listener inside our Navigation component. Add a new useState hook, and toggle the class in the following way:

Copied to clipboard! Playground
const [toggled, setToggled] = useState(false)

...

<div 
    className={toggled ? 'hamburger close' : 'hamburger'}
    onClick={() => setToggled(!toggled)}
>
    <span className="meat"></span>
    <span className="meat"></span>
    <span className="meat"></span>
    <span className="meat"></span>
</div>
Navigation.jsx
Toggle the .close class on the hamburger menu

Now the animation works, but it is still not aligned properly, due to some missing styles on our container. To adjust alignments, add the following rules to the CSS:

Copied to clipboard!
.container {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px;
}
navigation.scss
Display .container as flex
Required container styles
The required styles for the .container

Styling the menu

To create the dropdown effect, we are going to animate max-height properties. As height cannot be animated from 0 to auto, we are going to set max-height to 0 initially, and animate it to a higher value than our element can ever be. This can be done using the following rules:

Copied to clipboard!
.menu {
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-height: 0px;
    overflow: hidden;
    transition: max-height .6s ease-in-out;

    &.active {
        max-height: 500px;
    }
}
navigation.scss
Animating the max-height of the menu

To toggle the class from React, we need to edit the class of our ul inside the Navigation component. We can use the same toggled state that we have created for the hamburger menu:

Copied to clipboard! Playground
return (
    <nav>
        ...
        <ul
            className={[
                'menu',
                toggled && 'active'
            ].filter(Boolean).join(' ')}
        >
            {renderItems()}
        </ul>
    </nav>
)
Navigation.jsx
Toggle the .active class on the menu by using the toggled state

By using filter(Boolean).join(' '), we can ensure that no undefined or empty classes are added to the DOM. Alternatively, classnames is a popular package that is used for this purpose.

This sorts out the toggle functionality for the entire navigation, but we also have a toggle functionality for dropdown menus. To toggle dropdowns, extend the CSS for .menu with the following:

Copied to clipboard!
.menu li {
    font-weight: 500;
    cursor: pointer;
    position: relative;

    a:hover {
        background: #1E86D7;
    }

    a,
    span {
        display: flex;
        align-items: center;
        height: 100%;
        padding: 10px 20px;
        justify-content: space-between;
    }

    span img {
        transition: transform .3s ease-in-out;
    }

    span.toggled {
        img {
            transform: rotate(180deg);
        }

        + .sub-menu {
            max-height: 500px;
        }
    }
}
navigation.scss
Styles for toggling dropdown menus

To toggle dropdowns, we can add a .toggled class on the span elements. In this case, we want to rotate the arrow icons 180Β° and animate the max-height property of the .sub-menu next to it. We need to use the adjacent sibling selector (+) to select the dropdowns.

To toggle this class, we need to go back to our Navigation component and attach an onClick event listener to our span. Every time we click on the button, this will toggle the .toggled class:

Copied to clipboard! Playground
const toggleSubMenu = event => {
    event.currentTarget.classList.toggle('toggled')
}

{/* Inside `renderItems`, attach the function to an onClick listener */}
<span onClick={toggleSubMenu}>
    {item.name}
    <img src="/arrow.svg" />
</span>
Navigation.jsx
Toggling the toggle class on the span

There is only one thing missing to finish everything on mobile, and that is the styles for the .sub-menu elements. Extend the CSS with the following rules:

Copied to clipboard!
.sub-menu {
    max-height: 0;
    overflow: hidden;
    transition: max-height .5s ease-in-out;
    z-index: 1;

    li a {
        padding: 10px 40px;
        font-weight: 400;
    }
}
navigation.scss

Adding overflow: hidden, and max-height: 0 will ensure the dropdown is closed initially, and only expanded upon click (handled through the .toggled class).

πŸ” Login to get access to the full source code in one piece. Sass styles and navigation included using React Router.

Styling the Desktop Navigation

To add the desktop styles, we need to introduce a media query. This is the time when we want to switch the layout from column to row, and also hide the hamburger menu:

Copied to clipboard!
@media (min-width: 600px) {
    nav {
        flex-direction: row;
        gap: 50px;
    }

    .hamburger {
        display: none;
    }
}
navigation.scss
Change the layout and hide the hamburger menu

We also need to switch the layout from column to row for the .menu, and reset the overflow to visible. Here, we also want to animate max-height on hover (instead of click):

Copied to clipboard!
.menu {
    max-height: none;
    flex-direction: row;
    overflow: visible;
    gap: 50px;

    li {
        a,
        span {
            padding: 0 10px;
        }

        span img {
            display: none; // πŸ’‘ Also hide the arrows on desktop
        }

        span.toggled + .sub-menu {
            max-height: 0px;
        }

        &:hover .sub-menu,
        &:hover span.toggled + .sub-menu {
            max-height: 300px;
        }
    }
}
navigation.scss
Reset mobile styles, and animate max-height on hover

The last styles that we are missing are for the dropdown on desktop. To create the dropdown effect, we want to position them absolutely:

Copied to clipboard!
.sub-menu {
    position: absolute;
    left: -10px;
    background: #209AF1;
    border-bottom-left-radius: 5px;
    border-bottom-right-radius: 5px;

    li a {
        padding: 10px 20px;
    }

    li:last-child a {
        border-bottom-left-radius: 5px;
        border-bottom-right-radius: 5px;
    }
}
navigation.scss
Adding the last styles for the dropdown

We also want a negative value for the left property, equal to the padding-left of the span above it. This will ensure that the text is aligned properly with the menu item.


Closing Menu on Click

At this stage, the menu is working and animating properly. However, we can add some extra changes to make the navbar more user-friendly. Whenever the user clicks on one of the menu items, we want to close the entire menu.

For this, we are going to need a new function invoked whenever a Link is clicked. Add the following to both renderChildren and renderItems:

Copied to clipboard!
const renderChildren = children => (
    <ul className="sub-menu">
        {children.map((child, index) => (
            <li key={index}>
-               <Link to={child.url}>
+               <Link to={child.url} onClick={() => closeMenu(true)}>
                    {child.name}
                </Link>
            </li>
        ))}
    </ul>
)

const renderItems = () => items.map((item, index) => (
    <li key={index}>
        {item.url
-           ? <Link to={item.url}>{item.name}</Link>
+           ? <Link to={item.url} onClick={() => closeMenu()}>{item.name}</Link>
            : <span onClick={toggleSubMenu}>
                {item.name}
                <img src="/arrow.svg" />
                </span>
        }
        {item.children && renderChildren(item.children)}
    </li>
))
Navigation.jsx
Closing menu on clicks

The onClick on the dropdown is called with a true parameter, while on line:18 it is not. This will decide whether the click happens on the main navigation or on one of the dropdown elements. Let's see how the closeMenu function works:

Copied to clipboard! Playground
const [closeSubMenu, setCloseSubMenu] = useState(false)

const screenSizes = {
    small: 600
}

const closeMenu = (closeSubMenu = false) => {
    setToggled(false)

    if (closeSubMenu && window.innerWidth > screenSizes.small) {
        setCloseSubMenu(true)
        setTimeout(() => setCloseSubMenu(false), 0)
    }
}

...

return (
    <nav>
        ...
        <ul
            className={[
                'menu',
                toggled && 'active',
                closeSubMenu && 'closed'
            ].filter(Boolean).join(' ')}
        >
            {renderItems()}
        </ul>
    </nav>
)
Navigation.jsx
Implement the closeMenu function

Note that we can also call the setToggled updater function to toggle the menu off. This means the function will work for both mobile and desktop.  

We need a new useState hook for the new function. This will be responsible for handling another class on the ul (on line:25). This class will only be applied on desktop as we check the screen size in the closeMenu function.

To make the if statement more meaningful, we can define screen sizes in an object. The class only hides the dropdown after clicking and removes the class at the end of the call stack using a setTimeout.

Copied to clipboard!
.menu.close .sub-menu {
    display: none;
}
navigation.scss
Hiding the dropdown on click

Summary

If you have reached this far, congratulations! Now you have a working responsive, animated navbar in React, ready to be used in any project. Do you have experience with building navbars? Let us know your thoughts in the comments below! Thank you for reading through, happy coding! πŸ‘¨β€πŸ’»

100 JavaScript Project Ideas
  • twitter
  • facebook
React
Did you find this page helpful?
πŸ“š More Webtips
Mentoring

Rocket Launch Your Career

Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies:

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.