ReactT2 Dark_1200x303

In this tutorial, you will learn how to create a nested side navigation menu using recursive components. We will also cover how to style active nav links and create a layout using CSS grid.

There are many application types that might require you to create recursive components. If you have seen at least a few admin UI themes, you might have spotted that a lot of them often have a sidebar containing a navigation menu with nested links. In this tutorial, I want to show you how you can create a recursive menu in React. Below you can see a GIF of the menu we are going to create.

A recursive side menu with navigation items Home, Profile, Settings. Settings expands to include Account; Security > Credentials, 2FA.

Let’s start with a project setup.

Project Setup

For this tutorial, I decided to use Vite. You can scaffold a new project either with npm or Yarn.

With npm

npm init @vitejs/app recursive-menu --template react

With Yarn

yarn create @vitejs/app recursive-menu --template react

After the project is created, move into the project directory:

cd ./recursive-menu	

And install dependencies as well as the react-router-dom library

With npm

npm install react-router-dom

With Yarn

yarn add react-router-dom

Next, clean up App.jsx and App.css files. You can remove everything from the App.css file. Below you can see how your App.jsx file should look:

import React from 'react';
import './App.css';

function App() {
  return <div className="App"></div>;
}

export default App;

After that, you can start the development server by either running npm run dev or yarn dev.

Layout and Routes Setup

Before we focus on creating a recursive side menu, I want to show you how to create a layout using CSS grid. After we have the layout ready, we will start working on the sidebar menu.

Let’s start with creating a Layout component. It will render header, aside, main, and footer elements.

src/layout/Layout.jsx

import React from 'react';
import style from './layout.module.css';

const Layout = props => {
  const { children } = props;

  return (
  	<div className={style.layout}>
      <header className={style.header}></header>
      <aside className={style.aside}></aside>
      <main className={style.main}>{children}</main>
      <footer className={style.footer}></footer>
    </div>
  );
};

export default Layout;

As you can see in the code, we are using CSS modules. CSS modules provide a lot of flexibility as they are great for scoping CSS and passing styles around.

If you don’t know what CSS modules are, you can check out this link.

Let’s create the layout.module.css file as well. The .layout class will be a grid with two columns and three rows. The first column with the value of 18rem is specifically for the sidebar. The 80px rows are for the header and footer respectively.

src/layout/layout.module.css

.layout {
  display: grid;
  grid-template-columns: 18rem 1fr;
  grid-template-rows: 80px 1fr 80px;
  min-height: 100vh;
}

.header {
  grid-area: 1 / 1 / 2 / 3;
}

.aside {
  grid-area: 2 / 1 / 4 / 2;
}

.main {
  grid-area: 2 / 2 / 3 / 3;
}

.footer {
  grid-area: 3 / 2 / 4 / 3;
}

If you would like to learn more about CSS grid, you should check out this complete guide and the CSS Grid Garden game.

Next, we need to update the App.jsx to utilize the Layout component we just created and add a few routes.

import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import Layout from './layout/Layout.jsx';
import Home from './views/home/Home.jsx';
import Profile from './views/profile/Profile.jsx';
import Settings from './views/settings/Settings.jsx';

import './App.css';

function App() {
  return (
    <Router>
      <div className="App">
        <Layout>
          <Switch>
            <Route exact path="/">
              <Home />
            </Route>
            <Route path="/profile">
              <Profile />
            </Route>
            <Route path="/settings">
              <Settings />
            </Route>
          </Switch>
        </Layout>
      </div>
    </Router>
  );
}

export default App;

We have three routes for Home, Profile and Settings components. We need at least a few routes, as we want to be able to switch between different pages when we are done with the recursive sidebar menu. Next, create these three components.

src/views/home/Home.jsx

import React from 'react';

const Home = props => {
  return <div>Home page</div>;
};

export default Home;

src/views/profile/Profile.jsx

import React from 'react';

const Profile = props => {
  return <div>Profile page</div>;
};

export default Profile;

src/views/settings/Settings.jsx

import React from 'react';
import { Switch, Route, useRouteMatch } from 'react-router-dom';
import Security from './views/Security';
const Settings = props => {
  let { path } = useRouteMatch();
  return (
    <div>
      <Switch>
        <Route path={`${path}/account`}>Account</Route>
        <Route path={`${path}/security`}>
          <Security />
        </Route>
      </Switch>
    </div>
  );
};

export default Settings;

Home and Profile components do not have anything besides a bit of a text. However, in the Settings component, we have two nested routes—account and security. The former route renders just text, but the latter renders a Security component.

With this setup we have these 5 routes:

  • /
  • /profile
  • /settings/account
  • /settings/security/credentials
  • /settings/security/2fa

Now, let’s create the recursive menu.

Recursive Menu

Let’s start with installing heroicons by running npm install @heroicons/react, or yarn add @heroicons/react. Icons are a great way to improve the visual look of a sidebar navigation menu.

Next, we need to create menu config and sidebar files. We will export a sideMenu constant which will be an array of objects. Each object can contain these properties:

  • label – The text label displayed for the link
  • Icon – The Icon component displayed next to the label
  • to – The path for the router NavLink component
  • children – A nested array of links

If an object has the children property, then it is treated as a nav header. It will have a chevron icon to open and close nested links. If it doesn’t have any children specified, it will be a nav link.

src/layout/components/sidebar/menu.config.js

import {
  HomeIcon,
  UserIcon,
  CogIcon,
  UserCircleIcon,
  ShieldCheckIcon,
  LockOpenIcon,
  DeviceMobileIcon,
} from '@heroicons/react/outline';

export const sideMenu = [
  {
    label: 'Home',
    Icon: HomeIcon,
    to: '/',
  },
  {
    label: 'Profile',
    Icon: UserIcon,
    to: '/profile',
  },
  {
    label: 'Settings',
    Icon: CogIcon,
    to: '/settings',
    children: [
      {
        label: 'Account',
        Icon: UserCircleIcon,
        to: 'account',
      },
      {
        label: 'Security',
        Icon: ShieldCheckIcon,
        to: 'security',
        children: [
          {
            label: 'Credentials',
            Icon: LockOpenIcon,
            to: 'credentials',
          },
          {
            label: '2-FA',
            Icon: DeviceMobileIcon,
            to: '2fa',
          },
        ],
      },
    ],
  },
];

After we have the menu config ready, the next step is to create a sidebar component that will contain the recursive menu.

src/layout/components/sidebar/Sidebar.jsx

import React from 'react';
import style from './sidebar.module.css';
import NavItem from './navItem/NavItem.jsx';
import { sideMenu } from './menu.config.js';

const Sidebar = props => {
  return (
    <nav className={style.sidebar}>
      {sideMenu.map((item, index) => {
        return <NavItem key={`${item.label}-${index}`} item={item} />;
      })}
    </nav>
  );
};

export default Sidebar;

The sidebar component loops through the sideMenu config array we have specified before and renders NavItem component for each item. The NavItem component receives an item object as a prop. We will get to the NavItem component in a moment. We also need to create a CSS file for the sidebar.

src/layout/components/sidebar/sidebar.module.css

.sidebar {
  background-color: #1e40af;
  height: 100%;
}

We need to update the Layout component to include the Sidebar component we just created. Import it and render it in the aside element as shown below.

src/layout/Layout.jsx

import React from 'react';
import style from './layout.module.css';
import Sidebar from './components/sidebar/Sidebar.jsx';

const Layout = props => {
  const { children } = props;

  return (
    <div className={style.layout}>
      <header className={style.header}></header>
      <aside className={style.aside}>
        <Sidebar />
      </aside>
      <main className={style.main}>{children}</main>
      <footer className={style.footer}></footer>
    </div>
  );
};

export default Layout;

Great! We can focus on the NavItem component next. The NavItem component will check if the item object pass contains the children property. If it does, then it will return a NavItemHeader component. However, if there are no nested children links, then the NavItem will render the NavLink component from the react-router-dom library.

Note that we are using the NavLink component instead of the usual Link. The reason for this is because the NavLink component allows us to specify activeClassName, which is used to change the background color of the currently active link.

src/layout/components/sidebar/navItem/NavItem.jsx

import React from 'react';
import { NavLink } from 'react-router-dom';
import style from './navItem.module.css';
import NavItemHeader from './NavItemHeader.jsx';

console.log({ style });
const NavItem = props => {
  const { label, Icon, to, children } = props.item;

  if (children) {
    return <NavItemHeader item={props.item} />;
  }

  return (
    <NavLink
      exact
      to={to}
      className={style.navItem}
      activeClassName={style.activeNavItem}
    >
      <Icon className={style.navIcon} />
      <span className={style.navLabel}>{label}</span>
    </NavLink>
  );
};

export default NavItem;

The last component we need to create is the NavItemHeader component. This component is responsible for conditionally rendering nested links. It always renders a button with an icon and label specified in the config as well as the chevron icon. Besides that, it loops through the children array. If an item in the children array also has a children property, then another NavItemHeader component is rendered. Otherwise, the NavLink component is rendered.

src/layout/components/sidebar/navItem/NavItemHeader.jsx

import React, { useState } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import style from './navItem.module.css';
import { ChevronDownIcon } from '@heroicons/react/outline';

const resolveLinkPath = (childTo, parentTo) => `${parentTo}/${childTo}`;

const NavItemHeader = props => {
  const { item } = props;
  const { label, Icon, to: headerToPath, children } = item;
  const location = useLocation();

  const [expanded, setExpand] = useState(
    location.pathname.includes(headerToPath)
  );

  const onExpandChange = e => {
    e.preventDefault();
    setExpand(expanded => !expanded);
  };
	
  return (
    <>
      <button
        className={`${style.navItem} ${style.navItemHeaderButton}`}
        onClick={onExpandChange}
      >
        <Icon className={style.navIcon} />
        <span className={style.navLabel}>{label}</span>
        <ChevronDownIcon
          className={`${style.navItemHeaderChevron} ${
            expanded && style.chevronExpanded
          }`}
        />
      </button>

      {expanded && (
        <div className={style.navChildrenBlock}>
          {children.map((item, index) => {
            const key = `${item.label}-${index}`;

            const { label, Icon, children } = item;

            if (children) {
              return (
                <div key={key}>
                  <NavItemHeader
                    item={{
                      ...item,
                      to: resolveLinkPath(item.to, props.item.to),
                    }}
                  />
                </div>
              );
            }

            return (
              <NavLink
                key={key}
                to={resolveLinkPath(item.to, props.item.to)}
                className={style.navItem}
                activeClassName={style.activeNavItem}
              >
                <Icon className={style.navIcon} />
                <span className={style.navLabel}>{label}</span>
              </NavLink>
            );
          })}
        </div>
      )}
    </>
  );
};

export default NavItemHeader;

Finally, here are the classes that are shared between NavItem and NavItemHeader components.

src/layout/components/sidebar/navItem/navItem.module.css

.navItem {
  padding: 0.8rem 1.25rem;
  text-decoration: none;
  display: flex;
  align-items: center;
}

.navItem:hover {
  background-color: #1e3a8a;
}

.activeNavItem {
  color: #dbeafe;
  background-color: #1e3a8a;
}

.navIcon {
  color: #d1d5db;
  width: 1.5rem;
  height: 1.5rem;
  margin-right: 1rem;
}

.navLabel {
  color: #d1d5db;
  font-size: 1rem;
}

.navItemHeaderButton {
  width: 100%;
  outline: none;
  border: none;
  background: transparent;
  cursor: pointer;
}

.navItemHeaderChevron {
  color: #d1d5db;
  width: 1.5rem;
  height: 1.5rem;
  margin-left: auto;
  transition: all 0.25s;
}

.chevronExpanded {
  transform: rotate(180deg);
}

.navChildrenBlock {
  background-color: hsl(226, 71%, 36%);
}

After adding these styles, you should see the recursive side menu shown in the gif at the start of this tutorial.

That’s it. I hope you found this tutorial useful and have a better idea about how to implement a recursive menu in React. You can use this code in your own projects and expand on it. Recursively rendered components might be a bit intimidating at first glance, but it’s good to know how to implement them, as they can be very useful, especially in scenarios like the one we just covered. You can find the full code example for this tutorial in this GitHub repo.


Thomas Findlay-2
About the Author

Thomas Findlay

Thomas Findlay is a 5-star rated mentor, full-stack developer, consultant, technical writer and the author of “Vue - The Road To Enterprise.” He works with many different technologies such as JavaScript, Vue, React, React Native, Node.js, Python, PHP and more. Thomas has worked with developers and teams from beginner to advanced and helped them build and scale their applications and products. Check out his Codementor page, and you can also find him on Twitter.

Related Posts

Comments

Comments are disabled in preview mode.