I struggled for a while with mobile navigation menus. They are complicated.
There are lots of choices to make when is comes to design considerations. This is for the menu container itself and then things like the icon and the list items.
A mobile navigation menu really has four states to manage: mobile-active, mobile-inactive, desktop-active, desktop-inactive. And then there’s animation between these states.
Then there’s the actual implementation. You really need to know your CSS targeting as there is a lot nesting of lists within lists for submenu dropdowns. Animating a single-level menu is fairly easy but to toggle a submenu for a multi-level menu you have to target siblings and parent elements.
I still believe mobile navigation menus (along with forms) is one of the more difficult yet very important fundamentals a developer should learn well.
Using a library like (bootstrap, material-ui, tailwind, etc.) is the easiest solution by just pasting in code but a web developer needs to know how to code such basic web functionality from scratch! With that being said, if you are a trying to ship more apps to production, using a library may be a viable option.
As usual, all of your design and user-experience decision need to be in line with accessibility and screen readers.
The side drawer or from the top? if from the side from which side? Full screen or partial modal, the overly large circle. If partial modal, does the other content behind it stay fixed or get shifted down? Aldo if partial modal, if a user clicks outside of the menu, should it automatically close? Should your mobile be an entirely different menu from your desktop menu?
Full Screen non-scrollable: Apple, vercel, netlify, Robinhood, NFL, microsoft, cnbc, dribbble
Side drawer: duckduckgo, imdb, merriam-webster, tripadvisor, homedepot, walmart, webmd, dominos,
Slim SideBar Icon Menu: tictok,
Partial Screen From Top (Off Click Closes):
Partial Screen From Top (Off Click Doesnt Close): Github,
Almost Full Screen Modal: stripe,
Hold the Burger: automattic, dictionary/thesaurus, bbc, rottentomatoes.
Button Instead of Burger: Tesla,
Note: there is a separate posts on Full Screen Menus here.
Having a full screen mobile navigation menu is popular should be in a developer’s toolbox.
Top left 0, height and width 100%. Why not top left right bottom all zero? Saw another tutorial that used position absolute; top, left 0, 100% width and 100vh but I’m not certain 100vh is supported for all browsers.
Setting the width to 100vh works but may not be supported on all browsers. With this option you are probably going to want to space out your links as appropriate.
Three self-closing div elements. One self-closing div element with “:before” and “:after” pseudo elements. An icon from react-icons or another library. Create your own SVG duotone icon. On toggle do you swap it out for a different icon, or have an animation in the case of the self closing div. There are a lot of options.
Separate internal post on menu-icons here.
Also, do you use JavaScript when only CSS and HTML can do the trick? If you go with just CSS do you use the checkbox strategy event though it seems pretty “hacky”?
What’s the best animation approach, (using the “transform: scale” property works well and you may want to use in tandem with the visibility property as well)?
Should you bother with toggling opacity, (setting an “opacity” to zero does not collapse a div like toggling the “display” property to “none”).
Animating the “left” property from -100% is a technique I’ve seen for side drawers.
Move from a stacked vertical list to a horizontal
Media query for browser re-sizing has to be mobile first. Dimensions for breakpoints are debatable but the 768px value seems to be a very common value.
Avoid absolute positioning where possible right? Common to still see and use for submenus on desktop. Lately (2022) I’m seeing a lot of this even for mobile.
May need absolute positioning for full screen screen?
Typically do not want to hard-code height of header as this makes any future changes difficult.
Flexbox is typically the way to go.
Displaying the List Items
Hard-code or display programmatically?
I’ve seen mapping over an array of objects in a separate file, (Bria Design, JS Mastery). I like this way and seems easy to implement. Remember to add a “key” prop to the “li” element of to prevent an error.
You can also add an optionally additional “cName” key to your object with a special class (for example: “nav-links-mobile”) for mobile-only links, ().
Animating the List Items
I like the opacity animating in after slight delay (credit Kevin Powell).
Although I am yet to implement myself, I am a fan of the staggered animation in.
Full Width Clickable Area on Mobile?
On mobile should the list-item link’s clickable area be the full width of the mobile or more bound to the text?
When you need an arrow denoting a submenu do you use an icon or the CSS border trick hack? How do you animate the arrow to change direction, (my current strategy is to use a font awesome icon in the “::after” pseudo element for the anchor tag)?
A lot of mobile navigation bars do not take up the full height and width of the screen when active/toggled. For these cases you may want to close the mobile if the user clicks the portion of the screen that is not the mobile navigation bar or site-header. To accomplish this you can create a backdrop component that is only shown when the menu is active, sits behind the menu but above the page content and when this is clicked updates the state to close the menu. You could use a transparent background or a subtle alpha color, or the same color as the menu to give the illusion of fullscreen. One thing to consider here is you will have to define the clickHandler function for the backdrop in the navbar comp and then pass it to Backdrop comp as props. Setting the height of the backdrop to 100vh created a scrollbar and setting overflow-y to hidden on the backDrop and Nav didn’t remove it. This is becasue the top is set to 80, (the undeclared height of the site-header), and so we need to move set vh to 80. I tried setting height to calc(100vh-80) with and without the pixels but this did not work.
Implementing via a backdrop may not be as good as adding an event listener to the document and closing with any clicks outside the mobile menu. This is described below.
Hover and click are the two broad approaches to toggle a desktop submenu. Hover and click aren’t mutually exclusive necessarily, but I prefer to choose one to avoid buggy behavior. For me, click wins over hover for accessibility. I then choose to scrap hover to avoid buggy behavior introduced by implementing both, which includes the following:
Where you click an menu-item open, then hover no longer works for that menu. Then you click to close the sub-menu and now the menu stays open because you are hovering over the item to click it. This reason alone is enough to remove the hover effect for me.
An example:
const hamburger = document.querySelector(".hamburger")
const navLinks = document.querySelector(".navLinks")
const links = document.querySelectorAll(".navlinks li")
hamburger.addEventListener("click", () => {
navLinks.classList.toggle("open")
links.forEach(link => {
link.classList.toggle("fade")
})
})
Here is another look at using Vanilla JavaScript to select and toggle, (Note: this code can be seen in the “multi-level-nav-simple-example” project folder on my machine). This one is interesting as it uses browser-width, checking if the value is smaller than 768 pixels, and if so, and the link has children, then add an active class.
There’s some nested conditional logic and DOM traversal. On mobile it expands any containing list when clicked. On desktop there is a flyout below and then to the right. The CSS transitions are awful. It’s crazy but this is the menu that became the basis for the WP ECP Menu.
const li = document.querySelectorAll('li.dropdown a')
const btn = document.querySelector('.nav-btn')
const nav = document.querySelector('ul.nav')
btn.addEventListener("click", e => {
nav.classList.toggle("toggle")
})
li.forEach(each => {
if (each.nextElementSibling !== null) {
each.addEventListener("click", e => {
if (window.innerWidth < 768) {
e.target.parentElement.classList.toggle("active")
}
})
}
})
if you are going the click approach over hover for toggling the desktop this is necessity.
// handle if a desktop submenu is open & user clicks outside of menu
document.addEventListener("click", e => {
if (!e.target.parentElement.classList.contains("menu-item-has-children")) {
// The DOM selector needs to be defined here
const parentListItems = document.querySelectorAll("li.menu-item-has-children.d-active")
if (parentListItems.length) {
parentListItems.forEach(item => {
item.classList.remove("d-active")
})
}
return
}
})
WordPress navigation menus are dynamic, meaning you need to make sure if a list item becomes a parent list item that it automatically gets the on-click functionality for it’s submenu as needed. Adding more list-items or a second level to the menu via the WP admin panel should not break your menu.
It seems like the WordPress admin panel can support seemingly infinite nesting, (I tested at least 5 levels deep in the dashboard)?
How deep should a WordPress theme support navigation menu nesting, (three or four nested levels)? Not all themes support multi-layer nested menus and most do up to two layers. Is there a way to make that restriction in the WP admin UI so the user wouldn’t just do it anyway and break the page?
How can you target the link inside a list-item menu item that has children without selecting all the links with children? In other words when you expand one sub menu how do you not expand all sub menus without having to create custom classes manually or target specific ID’s?
Having logo, hamburger-menu and nav all in one container div: continue to struggle using anything other than display:none, display:block to transition and there is no animation available on that. Anytime I try something different I am left with a blank space of where the menu was and I can’t get rid of it without having a height which I am not interested in since I want to use flexbox and padding to center and space. Max-height set to zero gets me there and is able to be animated but the animation only works after initial open?
Putting the nav in a different div: this option works for mobile in that you can now animate
One more thing to consider is what happens when the window is resized if the mobile nav is active. Without configuring for this if the mobile nav is active/open and the window is resized then the styles for the nav/ul/links will be stuck on the mobile styles. A simple way is to reset styles back with media query. What sucks here is you are duplicating code (Wait just use multiple selector; something like primaryNav, primaryNav.active; note this didn’t work on first pass). This may be easier than listening for browser resize event or listening for window.innerWidth, (I believe these need to be optimized with Lodash via throttle and/or debounce). Jake Trent has an article here that explains how to handle and I was pretty spot on with his fix. In the article he talks about setting inside a UseEffect the browser’s native window.addEventListener and listening for a resize event that triggers a function defined above in your useEffect. He brings in useState because when react detects an update in State it will re-render the component. He also talks about how you need a cleanup/teardown function to remove the event Listener.
The only difference is the event handling and how to conditionally render classes based on the event. It is absolutely still just CSS that makes the navigation menu responsive given a piece of “isOpen/isActive” state.
Dynamic Class: Using a ternary operator inside of the className attribute, (isOpen ? “primaryNav primaryNav–active” : “primaryNav”).
Dynamically using CSS Modules: When using CSS Modules (out of the box with Next JS), you can’t use the hyphen and setting multiple classes is a little different. To set multiple classes in CSS Modules you have to use className={`${styles.one} ${styles.two}`}).
Video one here using CRA, styled components, framer motion.
Max has a video of course!
Cool trick I saw was to use the next router (router.pathname) in a custom “is active” function in an onClick that passes the link name and dynamically adds an active class for a nice effect.
Put a navbar in a layout component which wraps your entire app in the “_app.js” file
First off does the state of modal open or not needs to be in global state? Ultimately we need to style the body element and we can’t target this global style in next.js/ CSS Modules unless the state is in the global state. Correction, We can add and remove classes to a global element from within the component You just need to write the CSS in the global css file.
In “React for the rest of us” course he uses app-wide state and dispatch with Context.
Alternatively, I read that styled-jsx is the best way to handle this.
When using Link from “next/link”, clicking a link doesn’t reset the page so you have to manually close the menu somehow. Each List-item needs an onClick that updates the isOpen/isActive state to false. I tried adding to Link element and this did not work so use the list-item element. I used an if window.innerWidth < 768 check to prevent this from happening when the menu is desktop. Not usre if this is even needed, and if it is, if it needs lodash to throttle so save resources.
A mobile menu needs to be able to be closed by hitting the escape key as well.
The headache that is “preventing scroll” when the modal open Nothing is easy as per the steps in this article here.
The article isn’t the best edit or easy to follow but it talks about the Safari issue both setting the web kit overflow scrolling to touch property, And the fact that you want to set prevent default for some element.ontouchstart. This ontouchstart is something I had never heard of before and learned a little bit about.
This is another example as to why I need an iPhone or iPad for Safari browser compatibility checks.
Much Easier Than I Originally Thought
I feel like an idiot because I was banging my head against the wall All day yesterday when all I need to do is in the on-click handler function just have document.query selector(“body”).classlist.add(“menu_open”) and in the global CSS file put my styles for body.menu_open.
// prevent scroll if the menu is open
useEffect(() => {
if (isOpen) {
document.querySelector("body").classList.add("menu_open")
} else {
document.querySelector("body").classList.remove("menu_open")
}
}, [isOpen])
Could any more work on understanding like when the traditional approach is okay when you need to use useRef etc.
Now there’s at least two cases you still need to remove the class when certain things happen. One is when a the link is clicked and a second is when the window is resized etc.
For the link click case, these all have individual functions that change the state so this is already done for us.
For the window resizing that is going to require adding a “resize” listener on the window that triggers a handle resize function. Do this is in a useEffect with a teardown function and no dependency. In the function, when the window.innerWidth is greater than a certain width the isOpen state must gets switched back to false. May need to use throttle or denounce for performance reasons on resize per Jake Trent article above. I’m pretty sure the recess event listener is fairly resource-intensive. Also I’m not setting the browser with using state which It’s in this article and others I’ve seen I don’t think this is necessary.
// bring back scroll if menu open and browser resized
useEffect(() => {
window.addEventListener("resize", windowResizeHandler)
return () => window.removeEventListener("resize", windowResizeHandler)
}, [])
function windowResizeHandler() {
if (window.innerWidth > 768) {
setIsOpen(false)
}
}
Now that we can are controlling scroll behavior we need to handle the jerky behavior due to the scroll bar width disappearance. So we need to resolve that. It’s the article above mentions You need to detect the scroll bar with and set as padding-right to the body. To detect scroll bar width You subtract the document width from the window width. See the article for simple code.
We also still need to optimize for Safari.
Part of me thinks, “Full screen navbars are everywhere so I should be able to implement this pretty easily right?” Wrong.
There’s a lot that goes in to seemingly basic features and it may not always be worth going the non-library route. It’s about tradeoffs.
One menu I used worked totally fine, it just didn’t close if a user clicks outside the menu. Is this worth a major time detour on the actual goals of the site to add this feature? Or maybe I just make the backdrop transparent so I don’t have to deal with turning off scrolling and handling a disappearing scroll bar. This works.
You can also try not showing the individual components, or put in their own body like container that you can turn off. This way the body won’t be that tall.
Internal post of notes from Fireship video here, Kevin Powell video here, Brian Design here.
Quentin Watt Font Awesome. ShowMenu setShowMenu with useState set to false. Set onClick to arrow function that setShowMenu to !showMenu. Used conditional rendering in react. Created new menu let variable set to null (just instantiated). Have an if statement that says if(showMenu) menu=-div the menu div-. He added a menuMask. He used tailwind css.
This was a good video as well with useState and material UI.
This may have been the best tutorial I’ve seen yet as far as fundamentals. The styling is terrible and it’s not the most quality produced video but it shows how to build a menu that is nested multiple layers deep with vanilla JavaScript. There is linked codepen too which is nice.
There’s another video here that says it can create multi-level nav menu for mobile and desktop with just CSS. Haven’t watched it yet but may give it a go. And the codepen.
Here is a good non-video article.
Here is a simple solution using flexbox and no floats. This is what I should implement.
Video here on modal like Instagram/reddit where if hit refresh on modal using next/link. Uses react modal package.
Here is a “webdev simplified” video showing modals using react portals
This article links to codepens for inspiration