Notes on mobile/hamburger menu in web app

Last updated on 20 Jun. 2024

Quick Info

mobile-menu-v18-w-anim version is a working version of my trial HTML, CSS and JS mobile menu. It provides a fully functional mobile menu, has a single menu for header (regular) and mobile, uses a list within nav element and has simple animation for mobile menu button. I think the code is still somewhat simple. Webpage on GitHub Pages  ... GitHub repository .

Details

To get a better understanding of coding of mobile/hamburger menu in web apps, I am trying to convert the mobile menu in lesson04 of Dave Gray's Tailwind tutorial, ... Github source code to plain HTML, CSS and JavaScript. I am doing it in stages.

Given below are some notes from that trial.

One small issue with Dave Gray Tailwind tutorial's mobile menu is that if the window height is reduced then one cannot scroll down to menu items that are not in view. But I think such a scenario is too odd and so this small issue can be ignored.
---

✖ works really well. The HTML code is ✖.
An alternative is ✕: ✕
---

From the JavaScript code of a starter version of my code:
    // First time around mobileMenu.style.display is "". Don't know why.
    // From second time onwards the display value is as expected.
    mobileMenu.style.display === "none" || mobileMenu.style.display === ""
      ? (mobileMenu.style.display = "flex")
      : (mobileMenu.style.display = "none");
----

In the CSS code of a starter version (of my code):
/* Perhaps below commented code is simpler to select menu anchor elements.
But I thought of retaining the other code as an alternative way that works.
.menu_nav > a:link,
.menu_nav > a:visited
.menu_nav > a:hover
I have not tested the above but I have used the above way for mobile_menu_nav 
class further down in this file, and that works.  */

a.menu_link:link,
a.menu_link:visited {
  text-decoration: none;
  color: white;
}
----

Version using JavaScript window resize event: mobile-menu-v6-JS-winresize

mobile-menu-v6-JS-winresize version is a working version of my trial HTML, CSS and JS mobile menu. It is without animation, using window resize event to set style along with using click event on various mobile menu elements to set style in Javascript code. I think the code is somewhat simple. Webpage on GitHub Pages ... GitHub repository .

Why is window resize event code needed in above version? Styles added by JavaScript code are added inline which override media query CSS rules. One way to fix the issue is to handle the window resize event in JavaScript code and change the styles as needed, which is the option I took. 
Related article and some info from it: Javascript override of media query,  https://stackoverflow.com/questions/28035960/javascript-override-of-media-query :

The .hiddenclass is staying hidden because it is a inline style, and inline styles override nearly all other styles. You have two options, one is to override the inline style with a CSS, as described in this CSS Tricks post:
...
Or, use JS or JQuery to remove the inline style when the screen is resized:
...
----

I then explored solutions without using the window resize event handling code. I had gone through Brian Design's HTML, CSS and JS tutorial website ... source code on GitHub ... which used (what I refer to as dummy) CSS classes ('active' and 'is-active') which are toggled (added/removed) from classlist of some menu related elements by JS code, and CSS rules where two classes need to be matched for the rules to apply with one of these classes being one of the two dummy CSS classes. Related code snippet examples from the CSS file are given below:

 .navbar__menu.active {
    background: #131313;
    top: 100%;
    ...
  }

...

  #mobile-menu.is-active .bar:nth-child(2) {
    opacity: 0;
  }
-----

This effectively made the related CSS rules conditional on whether a dummy class is applied to some elements or not. This approach does not seem to face the problem of JS code added inline styles overriding CSS media query rules and so window resize event handler is not needed. I adopted this approach in my code.

First some references ...
----

To match a subset of "class" values, each value must be preceded by a ".".

For example, the following rule matches any P element whose "class" attribute has been assigned a list of space-separated values that includes "pastoral" and "marine":
 
p.marine.pastoral { color: green }
This rule matches when class="pastoral blue aqua marine" but does not match for class="pastoral blue".
----

Target an element if it has more than one class applied, https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors/Type_Class_and_ID_Selectors#target_an_element_if_it_has_more_than_one_class_applied has an example with some divs using multiple classes and the CSS having rules with two classes applied (e.g. .notebox.warning).
----

Now some examples from my code:
HTML:
          <button id="hamburger-button" class="hamburger_button">
            &#9776;
          </button>
          <button id="mobile-menu-close-btn" class="mobile_menu_close_btn">
            &#10006;
          </button>
---
JS code of click event handler:
  const toggleMenu = () => {
    hamburgerBtn.classList.toggle("is_open");
    mobileMenuCloseBtn.classList.toggle("is_open");
    mobileMenu.classList.toggle("is_open");
  };
---
CSS code:
.hamburger_button {
  display: block;
}

.hamburger_button.is_open {
  display: none;
}

.mobile_menu_close_btn {
  display: none;
}

.mobile_menu_close_btn.is_open {
  display: block;
}
...
@media screen and (min-width: 768px) {
  .hamburger_button {
    display: none;
  }

  /* The below code does not come into play if is_open class is there in the close button element's classlist.   The CSS specificity of the earlier rule with is_open for close button element is 0,2,0 is higher than the CSS specificity of below rule which is 0,1,0. So the earlier rule 'wins'. */
  .mobile_menu_close_btn {
    display: none;
  }

  /* One way to ensure the rule here has higher specificity is to use the id selector which makes the rule have specificity of 1,0,0. That's what code below does. Even if above code is not commented, the code below takes priority and comes into play. */
  #mobile-menu-close-btn {
    display: none;
  }
-----

The comments in above code explain the CSS specificity issue I ran into in the media query with the approach of using two classes matching for some CSS rules, and how using ID selector in media query and class selectors outside media query resolves the issue, as ID selector has higher specificity than class selector(s).  Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity .

Another point in above code is with respect to the following snippets:
HTML:
          <button id="hamburger-button" class="hamburger_button">
            &#9776;
          </button>
---
CSS:
.hamburger_button {
  display: block;
}

.hamburger_button.is_open {
  display: none;
}
---

If is_open class is in the class list of (attached to) the hamburger button element which has hamburger_button class associated with it in the HTML itself, then the second of the above CSS rules snippet comes into play (overrides the first rule) as it has higher specificity due to two classes matching the element than the first CSS rule. Thus the hamburger button is hidden (display: none). Even if the specificity of the two rules were equal, it is the second rule that would override the first.

If is_open is not in the classlist of hamburger_button then the second rule would not match it and so the first rule will apply. Thus the hamburger_button is shown (display: block).

-----


Can I write a CSS selector selecting elements NOT having a certain class or attribute?,  https://stackoverflow.com/questions/9110300/can-i-write-a-css-selector-selecting-elements-not-having-a-certain-class-or-attr

Pseudo-class :not helps in making conditional CSS code in my app. more readable, IMHO. See the snippets below with comments explaining the code:

/* Below code is a little complex as it gets applied only if mobile-menu-btn classlist does NOT have is_open. But the logic of the code becomes clear. If is_open class is not there, then mobile-menu-btn should be shown. Conversely, if is_open class is there (in its classlist), then mobile-menu-btn should NOT be shown (as we will show mobile-menu-close-btn in that case). */
.mobile_menu_btn:not(.is_open) {
  display: block;
}

/* .mobile_menu_btn {
  display: block;
} */

/* Note that below code has higher specificity than the above .mobile_menu_btn rule (without not) which is now commented. So below code will override above .mobile_menu_btn (without not) rule if is_open class is in classlist of mobile-menu-btn. */
.mobile_menu_btn.is_open {
  display: none;
}

----

Version using is_open CSS class and toggling it in JavaScript: mobile-menu-v10-isopen-css-cl

mobile-menu-v10-isopen-css-cl version is a working version of my trial HTML, CSS and JS mobile menu. It is without animation using is_open dummy CSS class and toggling it in classlist of mobile menu elements in Javascript code. It also uses :not pseudo-class in CSS. I think the code is still somewhat simple. Webpage on GitHub Pages ... GitHub repository .

The Dave Gray tutorial code uses two different menus - one for the header (horizontal menu) and another for the mobile/tablet drop-down. The Brian Design code uses one single menu which is shown either as horizontal menu or as vertical drop-down menu depending on window/screen width. 

Version using single menu nav for regular and mobile menu: mobile-menu-v15-single-nav

Next step was to modify my code to use a single menu. Given below are notes related to that.

mobile-menu-v15-single-nav version is a working version of my trial HTML, CSS and JS mobile menu. It is without animation, using is_open dummy CSS class and toggling it in classlist of mobile menu elements in Javascript code. It also uses :not pseudo-class in CSS and uses single menu nav for regular and mobile menu. I think the code is still somewhat simple. Webpage on GitHub Pages  ... GitHub repository .

top, bottom, left and right properties are margins of a positioned element from what it is relative to. These properties are ignored if there is no position property.
'position: relative' has a position relative to its normal position. 
'position: absolute' has a position relative to its nearest positioned ancestor.

https://developer.mozilla.org/en-US/docs/Web/CSS/position has a nice demo. It also provides more precise description of the position related properties including impact of z-index.

From https://developer.mozilla.org/en-US/docs/Web/CSS/z-index : "The z-index CSS property sets the z-order of a positioned element and its descendants or flex and grid items."

In mobile-menu-v10-isopen-css-cl version of my trial webpage, 'header' element has position sticky. 'section' element with id of mobile-menu and class of mobile_menu (in HTML), which ('section' element) is a child of 'header' element, has position absolute with top as 68px and width as 100%. This results in mobile-menu being shown just below header (whose height is 68px) and it takes up the whole window/screen width. These are the only position related CSS rules in the webpage. [I think now that using top as 100% instead of 68px will also work and would be a better way.]

In mobile-menu-v15-single-nav version of my trial webpage, 'header' element continues to have position sticky. 'nav' element with id="menu-nav" class="menu_nav" is a descendant of 'header' element. Its position property varies as follows:
a) In mobile/less than tablet (<768 px width) mode, if mobile menu has to be shown (is_open), position is absolute with top as 100%, left as 0px and width as 100%. This results in 'nav' element (menu) being shown just below header and it takes up the whole window/screen width. The left 0px specification is needed as 'nav' element has sibling element and a parent element taking up space to its left in the normal positioning.
b) In mobile/less than tablet (<768 px width) mode, if mobile menu has to be hidden (!is_open), 'nav' element is hidden using 'top: -9999px; left: 0px;' . If left: 0px is not used then the mobile menu continues to be hidden, however a horizontal scrollbar appears when the window width is made small and one can scroll to an empty area on the right, IIRC. Note that 'display: none' also works to hide the mobile menu.
c) In tablet and higher mode (>=768px width), in the media query section, for the mobile menu element (#menu-nav), we set the display to flex and flex-direction to row. But we need to override the earlier CSS rules having set position to absolute and top to either 100% or -9999px, and show the menu in a horizontal way in the header. This can be done by using 'position: relative; top: 0px;' in this same #menu-nav related CSS rule. Alternatively 'position: static;' can be used without specifying a new value for top as position: static ignores top, bottom, left and right property values. In my trial webpage, I have used 'position: static'.
----------

From: 10 Ways to Hide Elements in CSS, https://www.sitepoint.com/hide-elements-in-css/#8absoluteposition "The position property allows an element to be moved from its default static position within the page layout using top, bottom, left, and right. An absolute positioned element can therefore be moved off-screen with left: -999px or similar."

Brian Design's webpage (link provided in earlier part of this post) uses a mobile last design in the CSS. So before the media query part of (max-width: 960px), it uses display: flex for '.navbar__menu' which is the CSS class for the menu elements. It does not use position at this stage. Within the media query part of (max-width: 960px), it uses display: grid, 'position: absolute; top: -1000px;' along with 'z-index: -1;' for '.navbar__menu'. So by default in this media query part, the menu is hidden. When active (.navbar__menu.active), to show the menu, it uses a later CSS rule for '.navbar__menu' using 'top: 100%;' and 'z-index: 99;'. This later rule overrides the earlier rule when 'active' class is in the classlist of the menu element.

From: CSS position properties, https://university.webflow.com/lesson/position-floats-and-clear-settings?topics=layout-design : "A relative setting without other positioning attributes added (top, left, bottom, or right), will not be affected. This is because it’s relative to itself exactly as if you left it as static."

===================================================

Next step was to use simple animation for the mobile menu button. I wanted the animation to be simple to reduce complexity of the code. 

I have observed that display:inline with empty span, with span class having non-zero width and height and suitable background color in Brian Design code does not show anything. display: block shows it (which is what Brian Design's code uses).

I did some experimentation with empty span and non-zero width and height and suitable background color. I saw that display:inline does not show anything but display:inline-block or display:block shows bars of background color in empty span element position.

From: show background of empty span, https://stackoverflow.com/questions/16744094/show-background-of-empty-span : "By default spans are inline page elements (rather than 'block' elements). This means they won't take up any more space in the page than that assigned to them—for example, if you place text in them (as you have found). To achieve what you want, you need a little CSS to define a height and width for the span, but you also need to make it a block element so that it is rendered consistently."

From: Does height and width not apply to span?, https://stackoverflow.com/questions/2491068/does-height-and-width-not-apply-to-span : "Span is an inline element. It has no width or height." .. "You could turn it into a block-level element, then it will accept your dimension directives."

Hmm. So it seems that inline elements do not get impacted by width and height, at least in the way Brian Design's code uses span elements.
----

From: Transition Timing Function: ease vs ease-in-out?, https://teamtreehouse.com/community/transition-timing-function-ease-vs-easeinout : "Ease is like ease-in-out, except it starts slightly faster than it ends;"

In CSS declaration: 'transition: all 0.3s ease-in-out;' - all means apply to all changed properties. Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/transition
With reference to above transtion declaration which is applied to the mobile/hamburger button, the middle bar opacity change from default of 1 to 0 seems to be instantaneous while the translate and rotate of the upper and lower bars have a visible transition effect. I tested it with 30s duration instead of 0.3s and saw that the opacity change of middle bar also has a visible transition effect then. So even with 0.3s duration, the opacity change would be having a transition effect but I cannot catch it.
------------

In my implementation of mobile menu with animation in my trial web page, clicking on left edge of span elements shows an insertion cursor. If the spans are translated and rotated to show an X then clicking on what seems to starting edge of translated and rotated span, shows an insertion cursor.
Using 'caret-color: transparent;' for the spans associated CSS class (.bar) solves the issue - the insertion caret is not shown. Ref: https://www.tutorialspoint.com/how-to-hide-the-insertion-caret-in-a-webpage-using-css

But this problem does not seem to be there in Brian Design's webpage. Main difference in implementation between the two is the following code in Brian Design's webpage CSS:
  #mobile-menu {
    position: absolute;
    top: 20%;
    right: 5%;
    transform: translate(5%, 20%);
  }
----

In my implementation, to keep things simple, I have not used the above code and relied on the container flexbox to justify and align the contained items including the mobile menu button. Does the above code in Brian Design's webpage prevent the insertion caret problem for it? Don't know and don't want to invest time to check that out as I think using 'caret-color: transparent;' seems to be the appropriate and straight-forward way to fix the issue in my implementation.

The below articles are relevant to me in this trial webpage:
Should I use <ul>s and <li>s inside my <nav>s?, https://stackoverflow.com/questions/5544885/should-i-use-uls-and-lis-inside-my-navs and related article: Why should I embed navigation bar links into a list and then into a navigation bar? [duplicate], https://stackoverflow.com/questions/61014714/why-should-i-embed-navigation-bar-links-into-a-list-and-then-into-a-navigation-b 

I think I should use a list within the nav element as that seems to be the recommended way. Brian Design's webpage uses a list with the nav element.
--------

The display: inline-block Value, https://www.w3schools.com/css/css_inline-block.asp
My understanding of some points in above article:
Links (a element) are inline by default and so width decl. does not apply to them. If we use inline-block, width gets respected without line-break after element. If we use block, width gets respected but a line-break is added after element.

Brian Design's webpage uses 'display: table' but I have used 'display: inline-block' in my trial webpage.
--------

Making drop-down mobile menu item (li element) to take up full width of grid (ul element) container: Use 'grid-template-columns 100%' in grid ul element. Note that Brian Design's webpage uses 'grid-template-columns auto' for the grid ul element and 'width: 100%' for the li element. That works in Brian Design's webpage but does not work in my trial webpage. I think the reason could be related to Brian Design's webpage containing the grid ul element in many more flex containers but I am not sure. Anyway, I have a good solution by simply using 'grid-template-columns 100%' in grid ul element which seems to be very logical.

An issue I faced is vertically aligning the link within the list item (by default the link appears at the top of list item).

Vertical Align List Item Contents, https://forum.freecodecamp.org/t/vertical-align-list-item-contents/146598 suggests using flex and align-items center to center contents of a list item. Brian Design webpage does use "display: flex; align-items: center; justify-content: center;" for each list item and for each link inside each list item, IIRC. That's what I am also doing in my trial page. But I do wonder whether there is a simpler way. https://www.w3schools.com/css/css_align.asp provides some options (for vertically centering) other than flex but they seem roundabout. I also tried using 'vertical-align: middle;' but that did not work. 
----

From Basic concepts of flexbox, https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Basic_concepts_of_flexbox : "To create a flex container, set the area's display property to flex. When we do this, the direct children of that container become flex items. You can explicitly control whether the container itself is displayed inline or in block formatting context using inline flex or inline-flex for inline flex containers or block flex or flex for block level flex containers."
So 'display: flex' seems to create a block level flexbox. This is also mentioned in this article: CSS display Property Explained – Block, Inline, Flex, and Grid, https://codesweetly.com/css-display-property/ .
-------

In my trial webpage, clicking on menu nav even when mobile menu is not being shown results in click event firing and JS code toggling the is_open class from classlist of ... One way to solve the issue would be to check whether mobile button has display set to none and if so not toggling the is_open class. But then we have an unnecessary invocation of the click handler when user clicks on regular menu. To avoid that, we could add the event listener when the menu is opened and remove the event listener for the menu nav after closing the menu.

...
Brian Design page does not seem to have the issue. I think that's because the code listens only to mobile menu button clicks. Noted that Brian Design's webpage menu links to pages (e.g. '/', '/tech.html') and so clicking on the menu link when it is in mobile drop-down mode, seems to result in page reload which closes the mobile menu. In my trial webpage, the links are to internal links which do not result in page reload and hence the mobile menu is not closed. 

I think now I understand why Dave Gray's Tailwind tutorial uses different mobile menu and regular menu. The common click handler for mobile menu button as well as mobile menu has the following code:
    const toggleMenu = () => {
        mobileMenu.classList.toggle('hidden')
        mobileMenu.classList.toggle('flex')
        hamburgerBtn.classList.toggle('toggle-btn')
    }
---

I think the mobile menu will have Tailwind 'hidden' class, which corresponds to CSS 'display: none;', when the webpage is in tablet or larger width mode and so the click handler will not be invoked irrespective of where the user mouse-clicks. Note that Tailwind 'flex' class (CSS: 'display: flex;') is also toggled and so I think that when the CSS is set to 'display: none', the flex declaration also will be removed from the mobile menu classlist.
...
Notes on my implementation of adding the event listener when the menu is opened and removing the event listener for the menu nav when the menu is closed.

My code uses:
if (mobileMenuBtn.classList.contains("is_open")) {...}

EventTarget: removeEventListener() method, https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener : "Calling removeEventListener() with arguments that do not identify any currently registered event listener on the EventTarget has no effect." So I think we need not check if an event listener has been added before calling removeEventListener().

The simple implementation of above approach is largely working with one small quirk. If the user increases width of window when mobile menu is open, after hitting the tablet breakpoint, CSS media query results in mobile menu and mobile menu button not being shown. The event listener for the menu continues to be active as the click event handler is not called in this case due to user increasing window width to tablet breakpoint. Now the user is shown the regular horizontal menu. If user clicks on the menu, the handle click event function gets invoked which then toggles the is_open class in the classlist of mobile menu button and menu nav, and then the event listener for menu nav click is removed. Now if the user changes window width to less than tablet size, the mobile menu is not shown. But if the user changes window width to less than tablet size without any click on the horizontal menu when window width was tablet size or bigger, the mobile menu is shown (as is_open class is present in classlist of menu nav (and mobile menu button)). I think this is too minor a quirk for me to invest time on fixing it, as of now. In future, if this needs to be fixed, I can consider solutions like using window resize event. A more interesting and efficient approach may be to have an event handler for media query breakpoint change.

From: Testing media queries programmatically, https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Testing_media_queries : "The DOM provides features that can test the results of a media query programmatically, via the MediaQueryList interface and its methods and properties. Once you've created a MediaQueryList object, you can check the result of the query or receive notifications when the result changes."

A simple example is provided here: How TO - Media Queries with JavaScript, https://www.w3schools.com/howto/howto_js_media_queries.asp
...

Version using simple animation for mobile menu button: mobile-menu-v18-w-anim

mobile-menu-v18-w-anim version is a working version of my trial HTML, CSS and JS mobile menu. It provides a fully functional mobile menu, has a single menu for header (regular) and mobile, uses a list within nav element and has simple animation for mobile menu button. I think the code is still somewhat simple. Webpage on GitHub Pages  ... GitHub repository .
...
Lesson 04 of Dave Gray Tailwind tutorial does not hide dropped-down mobile menu when window width is increased from less than tablet size to tablet size and beyond. Media query classes don't seem to be applied to the mobile-menu. So the code seems to be dependent on JavaScript mouse click on mobile menu or mobile menu button event handler for hiding the mobile-menu.

Other Notes


--------


Stylesheets cascade — at a very simple level, this means that the origin, the cascade layer, and the order of CSS rules matter. When two rules from the same cascade layer apply and both have equal specificity, the one that is defined last in the stylesheet is the one that will be used.
----- 


/* All <li> elements with a class list that includes both "spacious" and "elegant" */
/* For example, class="elegant retro spacious" */
li.spacious.elegant {
  margin: 2em;
}
----
Based on: What's the difference between CSS classes .foo.bar (without space) and .foo .bar (with space), https://stackoverflow.com/questions/10036156/whats-the-difference-between-css-classes-foo-bar-without-space-and-foo-bar

.element .symbol {}
Applies to elements with class .symbol that is inside element with class .element.
...
.element.large .symbol {}
Applies to elements with class .symbol that is inside element with classes .element and .large
...
.element, .symbol {}
The declaration applies to .element and .symbol classes.
-------------

----

Comments