Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

Creating a multilevel dropdown menu in React

9 min read 2528

Creating A Multilevel Dropdown Menu In React

Multilevel dropdown menus are a staple of web design. With the ability to provide multiple options to select from, they make navigation bars dynamic and organized.

For any developer working in React or any React-based project like Gatsby or Next.js, this tutorial covers the step-by-step process of how to implement the dropdown feature in a React project. At the end of this guide, we will have the menu below:

Implementing A Dropdown Feature In A React Project

To follow this tutorial, ensure you have a basic understanding of React and confirm you have Node.js installed on your computer. Then, we can get started.

Setting up the React project

Let’s start by creating a new React project called react-multilevel-dropdown-menu by running the following command:

npx create-react-app react-multilevel-dropdown-menu

Once the project generates, navigate inside the project folder using cd react-multilevel-dropdown-menu or simply open the project with a code editor.

Then, run the npm start built-in command to start the project in development mode. The app should launch in the browser at http://localhost:3000.

The React project structure

Like every React project, we will decompose our project UI design into independent and reusable components as outlined below:

Decompose Your Project UI Design Into Independent And Reusable Components

The parent component, App, holds the logo and the Navbar component and the Navbar holds the MenuItems component. In the MenuItems, we have the individual items and the Dropdown component. The Dropdown also holds MenuItems which may also have Dropdowns.

We made a custom demo for .
No really. Click here to check it out.

From this breakdown, we will create four different components.

Creating the project files

Head over to the src folder and delete all the files except the index.js. Next, create a folder called components inside the src and add the following component files: App.js, Dropdown.js, MenuItems.js and Navbar.js.

In the App.js file, add the following starting code:

const App = () => {
 return (
  <header>
   <div className="nav-area">
    navbar content
   </div>
  </header>
 );
};

export default App;

Save the file. Now replace the content of the src/index.js file with the following:

import React from "react";
import ReactDOM from "react-dom";

import App from "./components/App";
// styles
import "./app.css";

ReactDOM.render(
 <React.StrictMode>
  <App />
 </React.StrictMode>,
 document.getElementById("root")
);

We’ve imported a CSS file to add “looks and feel” to our project. So let’s create an app.css file in the src folder. Then, copy the styles from the multilevel-dropdown-menu project and add them to the app.css file.

Save all the files and see the content of the App component rendered in the browser.

Rendering top-level menu items

Let’s start building by rendering the top menu items. To do this, we must get the menu data by creating a menuItems.js file in the src folder and adding the following:

export const menuItems = [
 {
  title: "Home"
 },
 {
  title: "Services"
 },
 {
  title: "About"
 }
];

Now, save the file.

If we recall from the project design, the App.js file holds the logo and the Navbar component that renders the menu items. With that, let’s update the App.js file so we have the following:

import Navbar from "./Navbar";

const App = () => {
  return (
    <header>
      <div className="nav-area">
        <a href="/#" className="logo">
          Logo
        </a>
        <Navbar />
      </div>
    </header>
  );
};

export default App;

In the code, we are using the <a> tag for the internal link for simplicity. Normally, we would use the Link or NavLink (for menu navigations) from the react-router-dom to link between internal pages.

Moving on, notice we imported the Navbar component. So, head over to the components/Navbar.js and add the following code:

const Navbar = () => {
 return (
  <nav>
   <ul className="menus">
    Nav Items here
   </ul>
  </nav>
 );
};

export default Navbar;

Next, import the menuItems data, loop through it, and then render them in the JSX:

import { menuItems } from "../menuItems";
const Navbar = () => {
 return (
  <nav>
   <ul className="menus">
    {menuItems.map((menu, index) => {
     return (
      <li className="menu-items" key={index}>
       <a href="/#">{menu.title}</a>
      </li>
     );
    })}
   </ul>
  </nav>
 );
};

export default Navbar;

Save the file and see the frontend. It should look like so:

Navbar And Menu Items

This is a basic navigation menu. Let’s go a step further and display a single-level dropdown next.

Rendering a single-level dropdown menu

Let’s head over to the menuItems.js file and modify the data to include a submenu like so:

export const menuItems = [
 //...
 {
  title: "Services",
  submenu: [
   {
    title: "web design"
   },
   {
    title: "web development",
   },
   {
    title: "SEO"
   }
  ]
 },
 //...
];

Here, we added a submenu to Services because we want to make it a dropdown. Let’s save the file.

At the moment, the Navbar is rendering the menu items in our code. If we take a look at the design once again, the MenuItems, which is a direct child to the Navbar, holds the responsibility to display these items.

So, modify the Navbar so we have the following:

import { menuItems } from "../menuItems";
import MenuItems from "./MenuItems";
const Navbar = () => {
 return (
  <nav>
   <ul className="menus">
    {menuItems.map((menu, index) => {
     return <MenuItems items={menu} key={index} />;
    })}
   </ul>
  </nav>
 );
};

export default Navbar;

In the code, we are passing the menu items data to the MenuItems component via the items prop. This is a process called prop drilling and is a basic React principle.

In the MenuItems component, we will receive the items prop and display the menu items. We will also check if the items have a submenu and then display a dropdown. So open the components/MenuItems.js file and add the following code:

import Dropdown from "./Dropdown";

const MenuItems = ({ items }) => {
 return (
  <li className="menu-items">
   {items.submenu ? (
    <>
     <button type="button" aria-haspopup="menu">
      {items.title}{" "}
     </button>
     <Dropdown submenus={items.submenu} />
    </>
   ) : (
    <a href="/#">{items.title}</a>
   )}
  </li>
 );
};

export default MenuItems;

In the code, we use the button element to open the dropdown menu. If we use a link tag instead, we must add a role="button" if we want assistive technology such as screen readers.

Also in the code, we imported the Dropdown component and passed the submenu items via the prop.

Let’s now open the components/Dropdown.js file and access the prop so we can render the submenu like so:

const Dropdown = ({ submenus }) => {
 return (
  <ul className="dropdown">
   {submenus.map((submenu, index) => (
    <li key={index} className="menu-items">
     <a href="/#">{submenu.title}</a>
    </li>
   ))}
  </ul>
 );
};

export default Dropdown;

Remember to save all the files.

To see the dropdown menus, open the src/app.css file and temporarily comment-out the display: none; part of the CSS:

.dropdown {
 ...
 /* display: none; */
}

We’ve added a display: none; to the dropdown to hide it by default and only open it when we interact with the menu.

Once we remove the display: none;, the menu should look like so:

Services Dropdown Menu

Good. We are getting there!

Toggling the Dropdown menu

Let’s now define the logic that detects when a dropdown menu item is clicked so we can dynamically display or hide the dropdown box. To do this, we must add a state and update it on the dropdown menu click.

In the components/MenuItems.js file, let’s update the code to include the state:

import { useState } from "react";
// ...
const MenuItems = ({ items }) => {
 const [dropdown, setDropdown] = useState(false);
 return (
  <li className="menu-items">
   {items.submenu ? (
    <>
     <button
      // ...
      aria-expanded={dropdown ? "true" : "false"}
      onClick={() => setDropdown((prev) => !prev)}
     >
      {items.title}{" "}
     </button>
     <Dropdown 
      // ...
      dropdown={dropdown} 
     />
    </>
   ) : (
    // ...
   )}
  </li>
 );
};

In the code, we’ve defined a state variable called dropdown with a default value of false and a setDropdown updater to toggle the state when the dropdown button is clicked, as seen in the onClick event.

This allows us to dynamically add value to the aria-expanded attribute to indicate if a dropdown box is expanded or collapsed, which is beneficial for screen readers. We’ve also passed the dropdown variable to the Dropdown component as a prop so we can handle the dropdown toggle.

Let’s open the components/Dropdown.js to access the dropdown prop and use it to dynamically add a class name when a dropdown menu is clicked.

const Dropdown = ({ submenus, dropdown }) => {
 return (
  <ul className={`dropdown ${dropdown ? "show" : ""}`}>
   {/* ... */}
  </ul>
 );
};

export default Dropdown;

The show class name is added when a dropdown is activated. And, we’ve added a style display: block; to display the dropdown in our CSS.

Now, we can return the display: none; back to the .dropdown class selector in the src/app.css:

.dropdown {
 ...
 display: none;
}

Let’s save our files. We should now be able to toggle our menu dropdown.

Toggling Menu Dropdown

Multilevel dropdown menu

Like the single level dropdown, to add a multilevel dropdown, let’s open the menuItems.js file and modify the data to include multilevel submenus like so:

export const menuItems = [
 // ...
 {
  title: "web development",
  submenu: [
   {
    title: "Frontend",
   },
   {
    title: "Backend",
    submenu: [
     {
      title: "NodeJS",
     },
     {
      title: "PHP",
     },
    ],
   },
  ],
 },
 // ...
];

After adding a submenu to the web development option and another submenu to the Backend option, save the file.

Rendering a multilevel dropdown menu

As seen in the design, a dropdown can also have menu items, another dropdown, and so on. For this, in the Dropdown component, we must delegate the rendering of the menu items to the MenuItems component.

Open the components/Dropdown.js file, import the MenuItems, and pass the submenu via the items prop:

import MenuItems from "./MenuItems";
const Dropdown = ({ submenus, dropdown }) => {
 return (
  <ul className={`dropdown ${dropdown ? "show" : ""}`}>
   {submenus.map((submenu, index) => (
    <MenuItems items={submenu} key={index} />
   ))}
  </ul>
 );
};

export default Dropdown;

If we save the files and test the dropdown, it works, but we will notice that the dropdown overlaps each other:

Overlapping Dropdowns

By clicking the web development submenu, we want to logically position its dropdown to the right. We can achieve this by detecting the dropdown depth level.

Detecting the menu depth level

Knowing the menu depth level allows us to do a couple of things. First, we can dynamically add varying arrows to show that a dropdown exists. Secondly, we can use it to detect a “second and above” level dropdown, hence logically positioning them to the right of the submenu.

Open the components/Navbar.js file and add the following above the return statement:

const depthLevel = 0;

Also, let’s ensure we pass the value to the MenuItems via a prop. Our code now looks like so:

// ...
 return (
  // ...
   {menuItems.map((menu, index) => {
    const depthLevel = 0;
    return <MenuItems items={menu} key={index} depthLevel={depthLevel} />;
   })}
  // ...
 );
// ...

Next, in the MenuItems component, we access the depthLevel and use it to display dropdown arrows:

const MenuItems = ({ items, depthLevel }) => {
 // ...
 return (
  <li className="menu-items">
   {items.submenu ? (
    <>
     <button
      // ...
     >
      {items.title}{" "}
      {depthLevel > 0 ? <span>&raquo;</span> : <span className="arrow" />}
     </button>
     <Dropdown
      depthLevel={depthLevel}
      // ...
     />

For the depthLevel greater than 0, we display a right arrow using an HTML entity name, &raquo;, else we add an .arrow class name to style a custom down arrow. In our stylesheet, we added the styles for the down arrow.

Notice also that we are passing the depthLevel to the Dropdown via prop; there we will increment it for the dropdown menus.

In the components/Dropdown.js file, access the depthLevel prop, increment it, and check if the value is greater than 1 so we can add a custom class to the dropdown. We now styled the class in our stylesheet.

Also, ensure that we pass the depthLevel to the MenuItems as a prop:

const Dropdown = ({ submenus, dropdown, depthLevel }) => {
 depthLevel = depthLevel + 1;
 const dropdownClass = depthLevel > 1 ? "dropdown-submenu" : "";
 return (
  <ul className={`dropdown ${dropdownClass} ${dropdown ? "show" : ""}`}>
   {submenus.map((submenu, index) => (
    <MenuItems 
     // ... 
     depthLevel={depthLevel} 
    />
   ))}
  </ul>
 );
};

export default Dropdown;

Let’s save the files and test the project.

Testing Multiple Dropdown Menus

Now that we can toggle the menus, we need a way to close the dropdown when we click outside of it.

Closing the dropdown menu when users click outside it

By clicking outside the dropdown menu, we want to close it. We can do this by setting the dropdown state to the default value of false. We will define a logic that detects a click outside of the dropdown.

Let’s open the components/MenuItems.js file and update the import to include the useEffect and useRef Hook like so:

import { useState, useEffect, useRef } from "react";

Next, we will use the useRef to access the DOM elements of the dropdown by passing a reference object to the target node:

const MenuItems = ({ items, depthLevel }) => {
 // ...
 let ref = useRef();
 return (
  <li className="menu-items" ref={ref}>
   {/* ... */}
  </li>
 );
};

export default MenuItems;

Then, add the following code above the return statement:

useEffect(() => {
 const handler = (event) => {
  if (dropdown && ref.current && !ref.current.contains(event.target)) {
   setDropdown(false);
  }
 };
 document.addEventListener("mousedown", handler);
 document.addEventListener("touchstart", handler);
 return () => {
  // Cleanup the event listener
  document.removeEventListener("mousedown", handler);
  document.removeEventListener("touchstart", handler);
 };
}, [dropdown]);

Let’s save our files and test our project. It works!

In the useEffect Hook, we check if a dropdown is open and then check if the DOM node that is being clicked is outside of the dropdown, then we close the dropdown. You can read more about detecting outside click in React here.

Toggling dropdown on a mouse hover for bigger screens

Let’s add the functionality that displays the dropdown when the user moves the mouse over the menu item.

In the components/MenuItems.js, update the li in the JSX to include the onMouseEnter and onMouseLeave events:

const MenuItems = ({ items, depthLevel }) => {
 // ...
 return (
  <li
   // ...
   onMouseEnter={onMouseEnter}
   onMouseLeave={onMouseLeave}
  >
   {/* ... */}
  </li>
 );
};

export default MenuItems;

Then, add the following event handler function above the return statement:

const onMouseEnter = () => {
 window.innerWidth > 960 && setDropdown(true);
};

const onMouseLeave = () => {
 window.innerWidth > 960 && setDropdown(false);
};

Save the file and test your project.

With the code, the onMouseEnter handler is invoked when the mouse pointer moves onto a menu item. And from there, we check if the interior width of the window is greater than 960px, then, we open the dropdown.

Whenever the mouse leaves the menu item, we invoke the onMouseLeave handler, which then closes the dropdown.

Conclusion

I’m glad we are here. Now we can implement a multilevel dropdown menu in our React project. With the implementation in this tutorial, we can add as many menus and submenus in the data file and the multilevel dropdown magically appears in the frontend.

However, we should be mindful of the levels of dropdowns we add so that users don’t see it as annoying.

If you have questions and or contributions, I’m in the comment section. And if you like this tutorial, ensure you share it around the web.

You can find the project source code from this GitHub repository.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

7 Replies to “Creating a multilevel dropdown menu in React”

  1. hello bro assalaam alaikum
    Thanks for your code.
    I have loved the code of your multi level dropdown in react.
    I have a query . please help me.
    How to open a page after clicking on the any dropdown menu item. Please share me the code.

  2. Hello Ibadehin Mojeed ,
    Thank you for this. but I just have concern.I was trying fetch the specific id of the json file whenever I clicked on specific menu items. ( I used a mulitlevel data with id for every menu and submenu.. ) but I am having an error about this

    1. Hi Van, you are welcome. In your case, you need to pass along the id to the onClick event.

      So instead of having this:
      “onClick={() => setDropdown((prev) => !prev)}”,

      you will have something like this:
      “onClick={items.id ? () => handleClick(items.id) : null}”.

      Then define an handleClick handler above the return statement like so:

      const handleClick = (id) => {
      console.log(id);
      setDropdown((prev) => !prev);
      };

      That way, you will have access to the dropdown button IDs. For the link IDs, ensure you add the same onClick to the link element. I hope that solves your code issues.

      Thank you.

  3. Hi Ibadehin Mojeed,

    I am finding this kind of coding everywhere! I am very grateful for this code but I got a question how do u enable the “Service” dropdown into a clickable button? since i want it to be able to go to the “/serivce” page, I have already try everything and i still cannot do it, can you help me?

    1. Hello Hikari,

      I’m sorry for the oversight. I just saw your comment. This kind of coding/implementation is the ideal way that is why you find it everywhere. See the nav bar menu for logrocket.com, gatsbyjs.com or google.com/chrome. However, we can easily enable a dropdown menu like “Services” into a clickable button while still showing the dropdown on hover. For this, I created a GitHub repo to show the implementation: https://github.com/Ibaslogic/react-multilevel-dropdown_v2/blob/main/src/components/MenuItems.js.

      The focus is the MenuItems component and the menuItems.js file that is holding the menu data. In the MenuItems component, we check if an item has a URL and submenu, then, we make it clickable while still showing a dropdown on hover. If no URL, we only show hover without linking the button. Else, we render a simple element.

      I hope this solves the coding problem you have. Thank you for reading through.

Leave a Reply