Search code examples
next.jsresponsive-designmedia-queries

How to implement responsive mobile nav menu with Next.Js?


So, I'm a bit confused on how to implement a responsive mobile navigation in Next.js. My problem stems from handling the menu toggle state both via Javascript and CSS.

My nav menu follows the following tutorial from Log Rocket, but the methods employed in the tutorial were written for React and do not account for how Next.js applies CSS class names. The plain React example depends on dynamicaly updating the CSS, while it seems that most Next.js approaches suggest toggling the element with useState. This works, but I lose the granularity of being able to control the navigation through media queries and the toggle button.

My current implementation checks a boolean useState value to show the menu, which I want to only apply to the menu at 780px or lower. If I start with a state of false, the whole menu isn't rendered including the button to toggle it. If I start with a state of true, then the mobile menu is rendered as expanded automatically, which I want to prevent. This also resuls in the toggle removing the menu altogether when the user isn't on mobile (e.g. if the menu is untoggled, and the user goes to a large screen size.

So, the question becomes how to prevent this unintended behavior? Or how to set the mobile menu default state to be hidden while preserving the click to toggle functionality?

import { useState } from "react";
import Link from "next/Link";
import SiteLogo from "../components/CompLogo";
import navStyles from "../styles/Nav.module.css";

const Nav = () => {
  const [isNavExpanded, setIsNavExpanded] = useState(true);
  return (
    <div>
      <nav className={navStyles.nav}>
        <SiteLogo />
        <button
          className={navStyles.hamburger}
          onClick={() => {
            setIsNavExpanded(!isNavExpanded);
            console.log("clicked");
            console.log(isNavExpanded);
          }}
        >
          {/* icon from heroicons.com */}
          <svg
            xmlns="http://www.w3.org/2000/svg"
            className="h-5 w-5"
            viewBox="0 0 20 20"
            fill="currentColor"
          >
            <path
              fillRule="evenodd"
              d="M3 5a1 1 0 011-1h12a1 1 0 110 
              2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 
              01-1- 1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
              clipRule="evenodd"
            />
          </svg>
        </button>
        <div className={navStyles.navigation}>
          {isNavExpanded && (
            <ul className={navStyles.menu}>
              <li className={navStyles.links}>
                <Link href="/">
                  <a className={navStyles.anchors}>Home</a>
                </Link>
              </li>
              <li className={navStyles.links}>
                <Link href="/map">
                  <a className={navStyles.anchors}>Map Page</a>
                </Link>
              </li>
              <li className={navStyles.links}>
                <Link href="/about">
                  <a className={navStyles.anchors}>About</a>
                </Link>
              </li>
            </ul>
          )}
        </div>
      </nav>
    </div>
  );
};

export default Nav

Here's my css:

.one {
  margin-top: 0;
  height: 100vh;
  width: 100%;
  background-image: url("../../public/images/My\ project.jpg");
  background-position: top;
  background-repeat: no-repeat;
  background-size: cover;
  display: flex;
  justify-content: left;
  align-items: center;
}

.oneAppointment {
  width: 55%;
  margin-left: 10%;
  text-align: left;
  /* display: flex; */
  /* justify-content: center; */
  /* flex-direction: column; */
  /* height: 40%; */
  /* background-color: #fff; */
  /* padding: 20px 40px; */
  /* border-radius: 6px; */
}

.oneAppointment h1 {
  font-size: 40px;
  font-weight: 700;
  /* color: #000; */
}

.abc {
  font-size: 56px;
  font-weight: 700;
  /* color: blanchedalmond; */
}

.heading {
  font-size: 40px;
  font-weight: 700;
  /* background-color: blueviolet; */
  margin-bottom: 10px;
  /* color: #000; */
}
.designer a {
  margin-top: 16px;
}

.designer {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 520px;
  width: 100%;
  background-image: url("../../public/images/fas.jpg");
  background-position: center;
  background-repeat: no-repeat;
  background-size: contain;
  /* background-color: blueviolet; */
}

.blogs {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 520px;
  width: 100%;
  background-image: url("../../public/images/fas.jpg");
  background-position: center;
  background-repeat: no-repeat;
  background-size: contain;
  /* background-color: blueviolet; */
}

.collection {
  margin-top: 0;
  height: 90vh;
  width: 100%;
  /* background-image: url("./website/images/pexels-yan-krukov-4458418.jpg"); */
  background-image: url("../../public/images/collec.jpg");
  background-position: top;
  background-repeat: no-repeat;
  background-size: cover;
  display: flex;
  justify-content: center;
  align-items: center;
}

.collections {
  width: 45%;
  margin-left: 45%;
  text-align: center;
  margin-top: -100px;
}

.shop {
  margin-top: 0;
  height: 90vh;
  width: 100%;
  /* background-image: url("./website/images/pexels-yan-krukov-4458418.jpg"); */
  background-image: url("../../public/images/shopp.jpg");
  background-position: top;
  background-repeat: no-repeat;
  background-size: cover;
  display: flex;
  justify-content: center;
  align-items: center;
}

.shopContent {
  width: 45%;
  margin-left: 45%;
  text-align: center;
  margin-top: -100px;
}

@media only screen and (min-width: 1px) and (max-width: 768px) {
  .heading {
    font-size: 36px;
    font-weight: 700;
    margin-bottom: 10px;
  }

  .one {
    margin-top: 0;
    background-image: url("../../public/images/mmy.jpg");
    background-position: left;
  }

  .oneAppointment {
    width: 30%;
    margin-left: 5%;
    text-align: left;
    margin-top: -80px;
  }

  .oneAppointment h1 {
    font-size: 28px;
    font-weight: 700;
    line-height: 55px;
  }

  .abc {
    font-size: 40px;
    font-weight: 700;
    /* color: blanchedalmond; */
  }

  .designer {
    text-align: center;
    height: 600px;
    width: 90%;
    margin-left: 5%;
    background-position: left;
    background-size: cover;
  }
  .blogs {
    margin-top: 100px;
    text-align: center;
    height: 600px;
    width: 90%;
    margin-left: 5%;
    background-position: left;
    background-size: cover;
  }
  .collection {
    /* margin-top: 0;
    height: 90vh;
    width: 100%; */
    /* background-image: url("./website/images/pexels-yan-krukov-4458418.jpg"); */
    /* background-image: url("../public/images/collec.jpg"); */
    background-image: url("../../public/images/colle.jpg");
    background-position: left;
    /* background-position: calc(-10px); */
    /* background-repeat: no-repeat;
    background-size: cover;
    display: flex; */
    justify-content: left;
    /* align-items: center; */
  }

  .collections {
    width: 70%;
    margin-left: 25%;
    /* background-color: aquamarine; */
    text-align: right;
    margin-top: -140px;
  }

  .shop {
    margin-top: -100px;
    /* margin-top: 0;
    height: 90vh;
    width: 100%; */
    /* background-image: url("./website/images/pexels-yan-krukov-4458418.jpg"); */
    /* background-image: url("../public/images/shopp.jpg"); */
    background-position: calc(-280px);
    /* background-repeat: no-repeat; */
    /* background-size: cover; */
    /* display: flex; */
    justify-content: left;
    /* align-items: center; */
  }
  .shopContent {
    width: 70%;
    margin-left: 25%;
    /* background-color: chartreuse; */
    text-align: right;
    margin-top: 0px;
  }
}
.nav {
  background-color: white;
  color: black;
  width: 100%;
  display: flex;
  align-items: center;
  position: relative;
  padding: 0.5rem 0 rem;
}

.navigation {
  margin-left: auto;
}

.menu {
  display: flex;
  padding: 0;
}

.links {
  list-style-type: none;
  margin: 0 1rem;
}

.links a {
  text-decoration: none;
  display: block;
  width: 100%;
}

.hamburger {
  border: 0;
  height: 80px;
  width: 80px;
  padding: 0.5rem;
  border-radius: 50%;
  background-color: white;
  cursor: pointer;
  transition: background-color 0.2s ease-in-out;
  position: absolute;
  top: 50%;
  right: 5px;
  transform: translateY(-50%);
  display: none;
}

@media screen and (max-width: 768px) {
  .hamburger {
    display: block;
  }
  .menu {
    position: absolute;
    top: 146px;
    left: 0;
    flex-direction: column;
    width: 100%;
    height: calc(100vh - 147px);
    background-color: white;
    border-top: 1px solid black;
  }

  .links {
    text-align: center;
    margin: 0;
  }
  .links a {
    color: black;
    width: 100%;
    padding: 1.5rem 0;
  }
  .links:hover {
    background-color: #eee;
  }
  .menu.expanded {
    display: block;
  }
}

Solution

  • Not Going By your code but a general guide -

    *Important - this is only for your understanding and may not be exactly the same as what your code is.

    For you, the width is 780px where you want to check and set mobile menu and desktop menu.

    Step 1 - you will have to check the width of the screen, you can do this with CSS media screen width but then your component will load all the data for the mobile menu as well as the desktop menu, so you can write a eventlistener of width change and do something as, (suppose width is a state) -

    {width > 780px ? <DesktopMenu /> : <MobileMenu /> }
    

    Now you can lazy load both components.

    Now coming to the mobile menu part -

    • The "Open Menu Drawer Button" should always be visible by default on mobile screen (toggle button to open).

    • Write a click event on openMenuDrawerButton to open a drawer that actually holds the menu layout and use CSS for slide effect or anything.

    • again use a state to check whether a drawer is open or not lets call this state as visibleDrawer and then you toggle this state with openMenuDrawerButton {visibleDrawer : true} and also with close button present in the drawer itself.

       {!visibleDrawer && < OpenMenuDrawerButton /> }
      

    to hide the opendrawerbutton after you have clicked it (opened the drawer).

    • once the drawer open, show a close button on drawer which will hide the drawer {visibleDrawer : false} and make OpenMenuDrawerButton appear at its place.

    Do write if you need more explanation.