Editor’s note: This post was last updated on 6 November 2023 to update code blocks and include tips for optimizing dropdown menu performance.
Multilevel dropdown menus are a web design staple. They make navigation bars dynamic and organized by providing multiple options to select from.
For any developer working in a React or React-based project like Gatsby or Next.js, this tutorial covers the step-by-step process for how to implement the dropdown feature.
At the end of this guide, we will have the menu below:
To follow along, ensure you have a basic understanding of React and confirm you have Node.js installed on your computer. Then, we can get started.
Multilevel menus are designed to reveal deeply nested navigations when we click or hover over submenu items, as shown in the GIF above. This design is ideal for a small to medium-sized business site or blog.
Mega menus, on the other hand, can reveal an entire website’s navigation at once without clicking on submenus. That is, a single expansion can reveal a deeply nested website menu. This design can be useful for large websites with many categories and subcategories — such as a retail site.
Let’s create a new React project with the create-react-app
CLI:
npx create-react-app react-multilevel-dropdown-menu
Then do the following:
cd react-multilevel-dropdown-menu npm start
Let’s visualize our project and break down the user interface into small pieces of components:
The numbered labels on the image above correspond to the following component names:
App
: The parent/root componentHeader
: Tenders the logo and navbar contentNavbar
: Renders the MenuItems
componentMenuItems
: Renders individual items and the dropdownDropdown
: Also renders menu itemsFrom this breakdown, we will create five different components. We will add more components later when we get start routing.
As we can see, this project renders the menu navigation in the top section of the page. Nonetheless, the same process would be applied to rendering the navigation in the sidebar.
In the src
folder, let’s ensure the file tree follows this structure:
... ├── src │ ├── components │ │ ├── Dropdown.jsx │ │ ├── Header.jsx │ │ ├── MenuItems.jsx │ │ └── Navbar.jsx │ ├── App.css │ ├── App.jsx │ └── index.js │ └── menuItemsData.js
In the src/App.jsx
file, let’s render some simple text:
const App = () => { return <div>App content</div>; }; export default App;
Save the file. Now replace the contents of src/index.js
with the following:
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; // styles import './App.css'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <App /> </React.StrictMode> );
We’ve imported a CSS file to add the look and feel we want for our project. Now, let’s copy the styles from the multilevel-dropdown-menu project to replace the styles in the src/App.css
file. Save all the files and see the contents of the App
component rendered in the browser.
Let’s start building by rendering the top-level menu items. To do this, let’s create a src/menuItemsData.js
file to hold the menu items:
export const menuItemsData = [ { title: 'Home', url: '/', }, { title: 'Services', url: '/services', }, { title: 'About', url: '/about', }, ];
If we recall from our project image in the beginning, the App.jsx
file will render the Header
component that holds the logo and the Navbar
component. So, in the components/Header.jsx
file, let’s add the following code:
import Navbar from './Navbar'; const Header = () => { return ( <header> <div className="nav-area"> <a href="/" className="logo"> Logo </a> <Navbar /> </div> </header> ); }; export default Header;
For now, we are using the <a>
tag for the internal link. Later in this guide, we will replace it with the Link
or NavLink
component from react-router-dom
.
Notice that we imported the Navbar
component. So, head over to the components/Navbar.jsx
and add the following code:
const Navbar = () => { return ( <nav className="desktop-nav"> <ul className="menus">Nav Items here</ul> </nav> ); }; export default Navbar;
Next, let’s ensure we update the App.jsx
file to render the Header
component:
import Header from './components/Header'; const App = () => { return ( <div> <Header /> </div> ); }; export default App;
If we save all files, we should see a logo and the navigation text displayed on the frontend.
Next, in the Navbar.jsx
file, import the menuItemsData
data, loop through it, and then render it in the JSX:
import { menuItemsData } from '../menuItemsData'; const Navbar = () => { return ( <nav className="desktop-nav"> <ul className="menus"> {menuItemsData.map((menu, index) => { return ( <li className="menu-items" key={index}> <a href={menu.url}>{menu.title}</a> </li> ); })} </ul> </nav> ); }; export default Navbar;
Save the file and check the frontend. It should look like this:
This is a basic navigation menu. Let’s go a step further and display a single-level dropdown next.
Let’s head over to the menuItemsData.js
file and update the data to include a submenu
for the Services
link like so:
export const menuItemsData = [ // ... { title: 'Services', url: '/services', submenu: [ { title: 'Web Design', url: 'web-design', }, { title: 'Web Development', url: 'web-dev', }, { title: 'SEO', url: 'seo', }, ], }, // ... ];
Here, we added a submenu
to the Services
link to make it a dropdown. Let’s save the file.
Note that you can ignore the forward slash /
in the submenu
URLs. In this case, it doesn’t matter if we add it or not. However, if you want to achieve a dynamic nested route, you shouldn’t include it.
At the moment, Navbar
is rendering the menu items in our code. If we take a look at the design again, the MenuItems
component, which is a direct child of Navbar
, holds the responsibility to render these items.
So, modify Navbar
so we have the following:
import { menuItemsData } from '../menuItemsData'; import MenuItems from './MenuItems'; const Navbar = () => { return ( <nav className="desktop-nav"> <ul className="menus"> {menuItemsData.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, which 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.
Open the components/MenuItems.jsx
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.url}>{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 role="button"
if we want assistive technology such as screen readers to work with our project.
Also in the code, we imported the Dropdown
component and passed the submenu
items via the prop. Let’s now open the components/Dropdown.jsx
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.url}>{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:
.desktop-nav .dropdown { ... /* display: none; */ }
We’ve added a display: none;
property to the dropdown to hide it by default and only open it when we interact with the menu.
Once we remove the display: none;
property, the menu should look like so:
Good. We are getting there!
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.jsx
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 components/Dropdown.jsx
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. We’ve also added a display: block;
style declaration in our CSS file to display the dropdown.
Now, we can return display: none;
back to the .desktop-nav.dropdown
class selector in src/App.css
:
.desktop-nav .dropdown { ... display: none; }
Let’s save our files. We should now be able to toggle our menu dropdown, like so:
Like the single-level dropdown, to add a multilevel dropdown, let’s open the src/menuItemsData.js
file and update the data to include multilevel submenu
components by updating the Web Development
link like so:
export const menuItemsData = [ // ... { title: 'Web Development', url: 'web-dev', submenu: [ { title: 'Frontend', url: 'frontend', }, { title: 'Backend', submenu: [ { title: 'NodeJS', url: 'node', }, { title: 'PHP', url: 'php', }, ], }, ], }, // ... ];
After adding a submenu
to the Web Development
link and another submenu
to the Backend
link, save the file.
We are aware that a dropdown item can also have menu items, another dropdown item, and so on. To achieve this design, we will recursively render the menu items. In the Dropdown
component, let’s delegate the rendering of the menu items to the MenuItems
component.
Open the components/Dropdown.jsx
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:
When we click the Web Development submenu, we want to logically position its dropdown to the right. We can achieve this by detecting the dropdown 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. Second, we can use it to detect a “second and above” level dropdown, hence logically positioning dropdowns to the right of the submenu.
Open the components/Navbar.jsx
file and add the following above the return
statement:
const depthLevel = 0;
Also, let’s ensure we pass the value to MenuItems
via a prop. Our code now looks like so:
// ... return ( // ... {menuItemsData.map((menu, index) => { return <MenuItems items={menu} key={index} depthLevel={depthLevel} />; })} // ... ); // ...
Next, in the MenuItems
component, we access 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>»</span> : <span className="arrow" />} </button> <Dropdown depthLevel={depthLevel} // ... />
For the depthLevel
greater than 0
, we display a right arrow using an HTML entity name, »
. Otherwise, we add an .arrow
class name to style a custom down arrow. In our stylesheet, we added the styles for the down arrow.
Notice that we are passing the depthLevel
to the Dropdown
via prop; there, we will increment it for the dropdown menus.
In the components/Dropdown.jsx
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 have added styling for the class in our stylesheet.
Also, ensure that we pass 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:
Now that we can toggle the menus, we need a way to close the dropdown when we click outside of 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.jsx
file and update import
to include the useEffect
and useRef
Hooks like so:
import { useState, useEffect, useRef } from "react";
Next, we will use 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.
Let’s add the functionality that displays the dropdown when the user moves the mouse over the menu item.
In components/MenuItems.jsx
, 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 = () => { setDropdown(true); }; const onMouseLeave = () => { 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, opening the dropdown. On mouse leave, the onMouseLeave
function is called and the dropdown is closed.
Whenever the mouse leaves the menu item, we invoke the onMouseLeave
handler, which then closes the dropdown.
In our project, currently, we are using the HTML <a>
tag to link internal pages. This causes a full page to reload whenever we click any of the menu items.
For a React application, we are expected to use optimized link tags like Link
or NavLink components
for this operation. Likewise, we need to ensure the navigation items point and render their respective pages, so we have the feel of a multipage application.
This is where routing comes in. We will use the React Router to keep track of the current URL and display different views based on the URL. Let’s install the library:
npm install react-router-dom@6
Next, we create a routes
folder to hold all the pages and create all the pages in it. Alternatively, you can access the page files on GitHub. For example, to create our About
page, we’ll create a routes/about.jsx
file and include the following code in it:
import React from "react"; const About = () => { return <h1>About page content</h1>; }; export default About;
You can do the same for the rest of the links in our menuItemsData.js
file.
In the src/index.js
file, let’s make the following changes:
// ... import App from "./App"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import Root from "./routes/root"; import ErrorPage from "./error-page"; import About from "./routes/about"; import Services from "./routes/services"; import WebDesign from "./routes/web-design"; import WebDev from "./routes/web-dev"; import Frontend from "./routes/frontend"; import Php from "./routes/php"; import NodeJs from "./routes/node"; import SEO from "./routes/seo"; const router = createBrowserRouter([ { element: <Root />, errorElement: <ErrorPage />, children: [ { path: "/", element: <App />, }, { path: "services", element: <Services />, }, { path: "about", element: <About />, }, { path: "web-design", element: <WebDesign />, }, { path: "web-dev", element: <WebDev />, }, { path: "frontend", element: <Frontend />, }, { path: "node", element: <NodeJs />, }, { path: "php", element: <Php />, }, { path: "seo", element: <SEO />, }, ], }, ]); const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <RouterProvider router={router} /> </React.StrictMode> );
We imported and stored our pages in a router
const using the createBrowserRouter
API. We also replaced the App.jsx
component with our RouterProvider
and passed router
into it.
You will also notice that we have the element
, errorElement
, and children
props in the createBrowserRouter
API. The element
props accept a Root
component. This Root
component is our “root route” and will contain the layout of our page.
The errorElement
accepts the page we want our app to display if a user navigates to a page that doesn’t exist. Finally, the children
props contain all our pages with the corresponding URLs.
In the routes
directory, create a file called root.jsx
and add the following code to it:
import { Outlet } from "react-router-dom"; import Header from "../components/Header"; export default function Root() { return ( <div> <Header /> <div className="content"> <Outlet /> </div> </div> ); }
Now when a user navigates to any page, the Header
will be present in it.
App.jsx
fileBecause we now have a root layout, there is no need to have the Header
component in the App.jsx
file again:
import React from "react"; const App = () => { return <h1>App page content</h1>; }; export default App;
Both the Header
and MenuItems
components use the <a>
tag to link internal pages. We will replace the tag with the Link
component from the router library. Open components/Header.jsx
, import Link
, and use it like so:
// ... import { Link } from 'react-router-dom'; const Header = () => { return ( <header> <div className="nav-area"> <Link to="/" className="logo"> Logo </Link> <Navbar /> </div> </header> ); }; export default Header;
Similarly, in components/MenuItems.jsx
, let’s import the Link
to replace the HTML <a>
tag:
// ... import { Link } from 'react-router-dom'; const MenuItems = ({ items, depthLevel }) => { // ... return ( <li className="menu-items" ref={ref} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} onClick={closeDropdown}> {items.submenu ? ( // ... ) : ( <Link to={items.url}>{items.title}</Link> )} </li> ); }; export default MenuItems;
If we save the file and test our application, we should now be able to navigate without a page reload.
Currently, when we click on either the Services
or Web Development
buttons, they toggle the visibility of their dropdowns. But sometimes, we may want a dropdown menu button like Services
to point to its own page, /services
, while still showing the dropdown items when we hover over on a large screen.
Because we have added functions to toggle the visibility on mouse hover, we can now turn the dropdown buttons into links. Let’s update the MenuItems.jsx
like so.
To do this, we must ensure the dropdown menu item that we want to turn into a link includes its URL path. For example, in the src/menuItemsData.js
, the Services
menu item has a URL link:
export const menuItemsData = [ // ... { title: 'Services', url: '/services', submenu: [ // ... ], }, // ... ];
Next, in the components/MenuItems.jsx
file, we will expand the conditional check. Presently, we only check if a submenu
is present to render a button element like so:
return ( <li className="menu-items" ref={ref} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} onClick={closeDropdown}> {items.submenu ? ( <> <button> {items.title}{' '} {/* ... */} </button> </> ) : ( <Link to={items.url}>{items.title}</Link> )} </li> );
Now, we will check not only the submenu
but also the URL, and then we will ensure the button is linkable like so:
const MenuItems = ({ items, depthLevel }) => { // ... const onMouseLeave = () => { setDropdown(false); }; const toggleDropdown = () => { setDropdown((prev) => !prev); }; return ( <li className="menu-items" ref={ref} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> {items.url && items.submenu ? ( <> <button type="button" aria-haspopup="menu" aria-expanded={dropdown ? "true" : "false"} onClick={() => toggleDropdown()}> <Link to={items.url}>{items.title}</Link> {depthLevel > 0 ? <span>»</span> : <span className="arrow" />} </button> <Dropdown depthLevel={depthLevel} submenus={items.submenu} dropdown={dropdown} /> </> ) : !items.url && items.submenu ? ( <> <button type="button" aria-haspopup="menu" aria-expanded={dropdown ? "true" : "false"}> {items.title} {depthLevel > 0 ? <span>»</span> : <span className="arrow" />} </button> <Dropdown depthLevel={depthLevel} submenus={items.submenu} dropdown={dropdown} /> </> ) : ( <Link to={items.url}>{items.title}</Link> )} </li> ); }; export default MenuItems;
From the code above, we first check if the menu item has a submenu and a URL. If it does, we render a button with the menu item title and a link to the URL. We also render a dropdown component and pass the submenu as props to it.
If the menu item has a submenu but no URL, we render a button with the menu item title and a dropdown component with the submenu as props. Finally, we render a link with the menu item title and the URL if the menu item has no submenu.
We are navigated to the Services page when we click on the Services
menu item. The same thing goes for the Web Development
menu item.
When we click a dropdown item to open its page, we want to close the dropdown panel. We will add a click event to the menu item to invoke a function to set the dropdown state back to the default false value:
const MenuItems = ({ items, depthLevel }) => { const [dropdown, setDropdown] = useState(false); // ... const closeDropdown = () => { dropdown && setDropdown(false); }; return ( <li // ... onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} onClick={closeDropdown} > {/* ... */} </li> ); }; export default MenuItems;
While our project will work both on desktop and mobile, user experience matters when designing navigation menus, and not just for menubar accessibility in React.
We can adjust the design of our navbar menu to a single card instead of having individual cards for each dropdown. We can also adjust the dropdown buttons, which are links, so that when the user clicks on the link text, it navigates them to the corresponding page.
When they click on the dropdown icon, it only toggles the visibility of the dropdown menu without navigating the user to the new page. For this to happen, we must build a navbar menu specifically for smaller screens.
So, for screens larger than 960px, our current Navbar
will be displayed to the user. But for screens smaller than 960px in width, our new navbar MobileNav
will be rendered. Let’s create and implement it.
In our components
folder, create a file called MobileNav.jsx
and include the following code:
import React, { useEffect, useRef, useState } from "react"; import { menuItemsData } from "../menuItemsData"; import MobileMenuItems from "./MobileMenuItems"; const MobileNav = () => { const depthLevel = 0; const [showMenu, setShowMenu] = useState(false); let ref = useRef(); useEffect(() => { const handler = (event) => { if (showMenu && ref.current && !ref.current.contains(event.target)) { setShowMenu(false); } }; document.addEventListener("mousedown", handler); document.addEventListener("touchstart", handler); return () => { // Cleanup the event listener document.removeEventListener("mousedown", handler); document.removeEventListener("touchstart", handler); }; }, [showMenu]); return ( <nav className="mobile-nav"> <button className="mobile-nav__menu-button" type="button" onClick={() => setShowMenu((prev) => !prev)}> Menu </button> {showMenu && ( <ul className="menus" ref={ref}> {menuItemsData.map((menu, index) => { return ( <MobileMenuItems items={menu} key={index} depthLevel={depthLevel} showMenu={showMenu} setShowMenu={setShowMenu} /> ); })} </ul> )} </nav> ); }; export default MobileNav;
From the code above, we have a Menu
button, that, when you click on it, will toggle the visibility of our mobile dropdown menu. We also use the useEffect
and useRef
Hooks to close the menu when the user clicks outside of it.
Now, let’s create the MobileMenuItems
component:
import { useState } from "react"; import { Link } from "react-router-dom"; import MobileDropdown from "./MobileDropdown"; const MobileMenuItems = ({ items, depthLevel, showMenu, setShowMenu }) => { const [dropdown, setDropdown] = useState(false); const closeDropdown = () => { dropdown && setDropdown(false); showMenu && setShowMenu(false); }; const toggleDropdown = (e) => { e.stopPropagation(); setDropdown((prev) => !prev); }; return ( <li className="menu-items" onClick={closeDropdown}> {items.url && items.submenu ? ( <> <button type="button" aria-haspopup="menu" aria-expanded={dropdown ? "true" : "false"}> <Link to={items.url} onClick={closeDropdown}> {items.title} </Link> <div onClick={(e) => toggleDropdown(e)}> {dropdown ? ( <span className="arrow-close" /> ) : ( <span className="arrow" /> )} </div> </button> <MobileDropdown depthLevel={depthLevel} submenus={items.submenu} dropdown={dropdown} /> </> ) : !items.url && items.submenu ? ( <> <button type="button" aria-haspopup="menu" aria-expanded={dropdown ? "true" : "false"}> {items.title}{" "} <div onClick={(e) => toggleDropdown(e)}> {dropdown ? ( <span className="arrow-close" /> ) : ( <span className="arrow" /> )} </div> </button> <MobileDropdown depthLevel={depthLevel} submenus={items.submenu} dropdown={dropdown} /> </> ) : ( <Link to={items.url}>{items.title}</Link> )} </li> ); }; export default MobileMenuItems;
The MobileMenuItems
component is almost the same as the MenuItems
component. The difference is that we have removed the onMouseEnter
and onMouseLeave
functions from the menu item. We also removed the “click outside” function, which we added earlier to the MobileNav
component.
It is important to note that, in our MenuItems
component:
<button type="button" aria-haspopup="menu" aria-expanded={dropdown ? "true" : "false"} onClick={() => setDropdown((prev) => !prev)}> <Link to={items.url}>{items.title}</Link> {depthLevel > 0 ? <span>»</span> : <span className="arrow" />} </button>
We now have the following in MobileMenuItems
:
<button type="button" aria-haspopup="menu" aria-expanded={dropdown ? "true" : "false"}> <Link to={items.url} onClick={closeDropdown}> {items.title} </Link> <div onClick={(e) => toggleDropdown(e)}> {dropdown ? ( <span className="arrow-close" /> ) : ( <span className="arrow" /> )} </div> </button>
We moved the onClick
function from the button to the icon. This is because, on mobile, we can’t hover on the dropdown menu button to trigger the dropdown to display. And if we were to click on it, we would be redirected to the page before the dropdown is visible. So now, when we click on the icon, our dropdown will be visible without us being redirected unless we click on the link text.
The e.stopPropagation();
in the toggleDropdown
function is also very important as this stops our dropdown menu card from closing anytime we click within the menu card.
Let’s create our MobileDropdown.jsx
:
import MobileMenuItems from "./MobileMenuItems"; const MobileDropdown = ({ submenus, dropdown, depthLevel }) => { depthLevel = depthLevel + 1; const dropdownClass = depthLevel > 1 ? "dropdown-submenu" : ""; return ( <ul className={`dropdown ${dropdownClass} ${dropdown ? "show" : ""}`}> {submenus.map((submenu, index) => ( <MobileMenuItems items={submenu} key={index} depthLevel={depthLevel} /> ))} </ul> ); }; export default MobileDropdown;
The MobileDropdown
component has the same code as the Dropdown
component. The only difference is that we import the MobileMenuItems
component instead of the MenuItems
component.
Let’s now import our MobileNav
into our Header
component:
import MobileNav from "./MobileNav"; import Navbar from "./Navbar"; import { Link } from "react-router-dom"; const Header = () => { return ( <header> <div className="nav-area"> <Link to="/" className="logo"> Logo </Link> {/* for large screens */} <Navbar /> {/* for small screens */} <MobileNav /> </div> </header> ); }; export default Header;
Viewing our changes in the browser, we can now see that our menu is responsive and accessible on both smaller and larger screens:
Enhancing the performance of your multilevel dropdown menu involves specific design considerations. Here are some tips you can do to improve their user experience:
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 we don’t overwhelm users.
If you have questions or contributions, I’m in the comment section. And if you like this tutorial, please share it!
You can find the project source code in this GitHub repository.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Web components are underrated for the performance and ergonomic benefits they provide in vanilla JS. Learn how to nest them in this post.
LogRocket’s Galileo AI watches every user session in your app to surface the most impactful areas of user struggle and key behavior patterns.
Josh Schoonmaker shares his approach to balancing two key leadership traits — assertiveness and kindness — and knowing when to apply each.
Angular’s new defer
feature, introduced in Angular 17, can help us optimize the delivery of our apps to end users.
26 Replies to "How to create a multilevel dropdown menu in React"
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.
Thank you, Anjum. With React router, you can navigate between different routes. You can take a look at this article to get started: https://blog.logrocket.com/react-router-dom-tutorial-examples/
Hello, can you share a code please, I don’t find the information
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
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.
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?
I am having the same problem and it is a nightmare 😭😭😭
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.
I’m very grateful for this tutorial, it was just what I needed.
HOWEVER, like Anjum, I’m struggling getting things to happen when I click on the leaf nodes of the menu structure. I’m struggling thru the react-router tutorial that you referred Anjum to, but it would be better if you could enhance this tutorial to explain that. That tutorial is even longer than this one.
Hi Tim, I will talk with the LogRocket team and see how we can update the article to cover routing. Thank you all for following.
I got a simple multi-level menu bar working in my app with a bit of effort. I’m using “<a>” elements to switch between screens in my app and that mostly worked but causes problems because it causes a full page refresh (so I lost state and so on). So then I replaced it with “” and that solves the page refresh problem, but it introduces a new problem that the dropdown sticks around too long.
Hello brother
please help me.
How to open a page after clicking on the any dropdown menu item. Please share me the code.
Slaam bro,
brother its not working on small screen even i have install your clone project fom github.
Kindly guid me if u can how to resolve the issue.
Waiting for your response
Thanks
Salam Imran,
I just deployed the code to Heroku, https://multilevel-dropdown.herokuapp.com/ and it works as expected. Be aware that in the final code, we enabled a single-level dropdown on a smaller screen for a better user experience. See the last section of the article. You can ignore the last section if you still want a multi-level on a smaller screen. This article showcase all the different logic you can enable for a dropdown. Please follow the article without missing any sections. Thank you.
If the live URL above doesn’t work, see another one here: https://ibaslogic.github.io/react-multilevel-dropdown-menu/
I needed this tutorial, so I’m grateful
I need something else though, can you share with me how to be able to travel through the menu with up and down arrow buttons?
Hello. Nice code, thanks. But onClick={closeDropDown} does not allow the submenu to open. Is that true?
it is well guided source keep it up and thanks for sharing
Hi Ibadehin Mojeed,
Your tutorial was so helpful. I have a scenario like the list menu should be displayed to left and if we click on the parent element the dropdown must be displayed and that must not have the mouseevnts and for the sub-sub-menu no need of click event but it requries mouse events could you suggest me on this.
Thanks in advance
Pasindu Induwara,
hello brother, Great work. this helped me a lot for one of my assignment. and it worked perfectly well. and guided perfectly as well. Thank you for sharing your knowledge brother. well done.
Thanks man, you simply helped me a lot with this project, congrats!
This guide is very poorly written in my opinion. Critical steps are being left for the user to figure out, but also the naming of variables, files and overall structure of files doesn’t make any sense whatsoever, adding to the confusion of something which should be an easy matter.
Besides, why are they so many different files just passing information one to each other ?
Better would be to pack all menuItems in one file and one place, instead of this reach-around method shown here.
Hello Stefan,
Thank you for taking the time to read and provide feedback. Kindly note that this guide assumes a foundational understanding of React. React, being a library based on components, is why we have various component files. I assure you, that constructing a multi-level dropdown capable of seamless scalability to any level of nesting requires careful organization of files, as demonstrated in this tutorial.
We have endeavored to cover various logical aspects that come into play when developing such a UI. Once more, your input is greatly appreciated.
I have this set up and it works nice, but I do have one issue that I am stumped on. I would like an open menu dropdown to close when another option is selected, but currently both of my menus are still open. The only way to close them is to click on the main navbar menu head again to swap the aria-expanded to false.
Any ideas on how to approach? I think my methods do not work because they are always going off of the element that is clicked, and not looking for the one that was set to true.
Hi Matt,
Apologies for the oversight. Regarding the code issue you raised, it appears there might be an error on your end. After reviewing the code, I didn’t encounter the scenario where selecting an option fails to close another open menu.
I recommend cloning the project to observe its functionality and then making adjustments as necessary.
Clone the project using the following command:
git clone https://github.com/Ibaslogic/react-multilevel-dropdown-menu
If the problem persists, please share the code of your project, and I’ll be happy to assist.
Thank you.
I pulled down your repo, and I am starting to see the differences between what my predecessors made to the code compared to the pulled down version. Our menus work on clicking them, and that is what sets the value to TRUE to display them forcing the user to click the menu item again to return it back to false to hide it. I am going to try to rework it some and see if I can get it behaving like we need it to.
I appreciate you getting back to me, and sharing your code.