Hiding and Showing a WordPress Menu II- JavaScript Only, Please
Okay, so this is a continuation of a previously written tutorial, Hiding and Showing a WordPress Menu on Mobile Devices. In that tutorial, I described how to create a JS controlled dropdown menu that turns into a button in mobile devices. This tutorial assumes some knowledge of JavaScript, but since I learned this just recently, it shouldn’t be too hard to follow for my readers as well. Also, you may want to read the previous tutorial, as menu creation and setup is explained there.
This tutorial has been updated as of May, 2020 to reflect the latest code in The M.X., the theme that this tutorial references.
In my newest theme now under construction, I created a variation of a hide/show menu using JavaScript only (with a lot of help from the _s starter theme that it uses). I learned quite a bit from how the theme has its menu code setup. Making some adjustments, I adapted the menu design in tablet view and wider to be similar to the design in the previous tutorial, but without jQuery. Without further ado, let’s get busy.
Let’s Look at Underscores’ (_s) Navigation Code
The navigation code for Underscores is what places the button onto the page in mobile view. The screenshot below shows the basic behavior of the menu toggle button. When clicked, it reveals a menu. When clicked a second time it hides the menu.
Below is the link to the latest code for navigation.js in _s.
In the code above, to display the menu, the unordered list is set to display: none.
In my theme, I set the menu to display: flex
instead, as the theme uses flexbox for layout. You may want to leave it as is or set the ul to a different display method, such as block if you want users with JavaScript disabled to see the menu’s contents. I adjusted the navigation.js in my theme to hide the menu on load this way:
menu.setAttribute( 'aria-expanded', 'false' ); // set initial menu state here, instead of CSS file, in case JavaScript is turned off in browser. var windowWidth = window.innerWidth; if ( windowWidth < 600 ) { menu.style.display = 'none'; }...
The new method for setting the initial menu to be hidden is explained later below.
Looking at the navigation.js file linked to above, a few explanations are in order, as this is the method we will use with our expanded menu system later.
var container, button, menu;
… Sets up variables we can define later in the script.
container = document.getElementById( 'site-navigation' );
… Finds the element with the id #site-navigation
, which holds our button.
button = container.getElementsByTagName( 'button' )[0];
… Looks for the first (and only) instance of a button within the container, as denoted by the [0]
array location. If you wanted to find a second button in the container, you would use [1]
, for example.
menu = container.getElementsByTagName( 'ul' )[0];
… Looks for the first ul tag within the container
if ( -1 === menu.className.indexOf( 'nav-menu' ) ) { menu.className += ' nav-menu'; }
This bit of code checks to see if the menu’s ul does not contain the string “nav-menu”. If not, it is added with the className
property with the plus sign (+). The plus operator followed by the equals sign adds the class (with a space before it) to any existing classes.
button.onclick = toggleMenu; function toggleMenu() { if ( -1 !== container.className.indexOf( 'toggled' ) ) { container.className = container.className.replace( ' toggled', '' ); button.setAttribute( 'aria-expanded', 'false' ); menu.setAttribute( 'aria-expanded', 'false' ); menu.classList.add( 'hide' ); } else { container.className += ' toggled'; button.setAttribute( 'aria-expanded', 'true' ); menu.setAttribute( 'aria-expanded', 'true' ); menu.classList.remove( 'hide' ); } };
We will use the same method from Underscores to check if the menu’s container has the class toggled
. if so, we will replace the class with an empty string, otherwise (else), add the class toggled
. The aria-expanded attributes are to assist users using screen readers.
We also will hide the menu initially with a CSS class. The .hide
class will be created in the CSS section later.
Navigation.js also has similar functions for focus based navigation, but the code above is enough for us to work with.
Styling the Button
Underscores starts you off with a button that reads “Primary Menu”. That’s fine if you want that for the button’s style. I think a hamburger menu symbol as a replacement would work well here. For my theme, I used Google’s Material Design icons. You can use that or an embedded font icon. Below I’ll show examples for Material Design and Font Awesome. In header.php, add (or replace in Underscores based theme):
<button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false" title="<?php esc_attr_e( 'Toggle the navigation menu' ) ?>"><i class="material-icons">menu</i></button> <?php wp_nav_menu( array( ... ) ); ?>
or…
<button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false" title="<?php esc_attr_e( 'Toggle the navigation menu' ) ?>"><i class="fa fa-bars" aria-hidden="true"></i></button> <?php wp_nav_menu( array( ... ) ); ?>
The button should go directly above your wp_nav_menu
generated code.
If you are using Underscores, button styles are created for you. If you are using a starter theme with no button styles, you can give button a finger friendly design with a CSS width
and height
of 72px or 4.5em. Any other styling is probably best left to your theme.
The Navigation Code
The code that controls the expanded horizontal menu can be placed in a separate script or in the provided navigation.js script.
But first, in style.css, let’s layout the menu for larger screens.
Your theme may have some breakpoints setup already. Increase your browser to a width larger than the breakpoint for tablets (in my theme it is ~600px). Firefox’s Responsive Design View or Chrome’s Developer Tools are good to use for this purpose. In your style.css breakpoint for tablets, change the code as shown in the pseudocode:
For Flexbox:
@media screen and (min-width: 600px) { .menu-container ul { display: flex; align-items: flex-end; flex-flow: row wrap; } }
With Inline-Block:
@media screen and (min-width: 600px) { .menu-container ul > li { display: inline-block; vertical-align: top; } }
.menu-container
refers to the class applied to the nav
element wrapped around the menu.
The code above is very simple baseline CSS to get your list items aligned horizontally. Any other theming you do is dependent on how you would like your theme’s menu to look. I set a min-height and a dark background on the parent container for my theme.
Now on to the JavaScript. You can place the following code in navigation.js or another file. In my theme, it is in js/the-mx-scripts.js.
In the newer versions of The M.X., I separated the previous tutorial code into three functions, shown below:
function addDesktopNavButtons() { } function toggleMenuItems() { } function loadInitMenuState() { }
At the bottom of the file, we will load the functions when the page loads.
function themeNameInit() { addDesktopNavButtons(); toggleMenuItems(); loadInitMenuState(); }
document.addEventListener( 'DOMContentLoaded', themeNameInit );
Add the following and save to your js or scripts directory:
/* Add buttons to the navigation menu */// setup variables// Global variables var windowWidth; var menu = document.querySelector( '.main-navigation ul' ); function addDesktopNavButtons() { var hasChildren = document.querySelectorAll( '.main-navigation .page_item_has_children' ); var hasChildrenLink = document.querySelectorAll( '.main-navigation .page_item_has_children > a' ); var customHasChildren = document.querySelectorAll( '.main-navigation .menu-item-has-children' ); var customHasChildrenLink = document.querySelectorAll( '.main-navigation .menu-item-has-children > a' ); }
Setting up some variables, we’re using document.querySelectorAll
to choose all of the elements on the page that have a certain class attached to it. querySelectorAll can be used to select multiple elements with specified class names. For instance, in WordPress you may have article with a class of post. You can select this for muliple page types by comma separating each class Ex. document.querySelectorAll( '.blog .post, .archive .post, ... ');
querySelectorAll returns a node list, which is unusable for applying manipulations to each element returned, so we must first loop over each element and then apply some manipulations. Right after the previous code, add:
if ( windowWidth >= 600 ) {// For custom menus for ( var i = 0; i < customHasChildren.length; i++ ) { // Add button HTML after each link that has the class .menu-item-has-children customHasChildrenLink[i].insertAdjacentHTML( 'afterend', '<button class="menu-down-arrow"><i class="material-icons">arrow_drop_down</i></button>' ); } // For page menu fallback for ( var i2 = 0; i2 < hasChildren.length; i2++ ) { // Add button HTML after each link that has the class .page_item_has_children hasChildrenLink[i2].insertAdjacentHTML( 'afterend', '<button class="menu-down-arrow"><i class="material-icons">arrow_drop_down</i></button>' ); }} // closes windowWidth if statement
The above code uses the insertAdjacentHTML
method to add the html string for a button after customHasChildrenLink (a link with the class .menu-item-has-children).
Next, we add click events to the submenus. These will later be hidden and shown with the .toggled-submenu
class. Add the following right below the previous for loop:
function toggleMenuItems() { /* The code below roughly follows the Vanilla JS method in the article "Lose the jQuery Bloat" */ /* https://www.sitepoint.com/dom-manipulation-with-nodelist-js/ */ // loop through each element that has .sub-menu var customSubmenuButton = document.querySelectorAll( '.main-navigation .menu-down-arrow' ); for ( var iSub = 0, customSubmenuButton; iSub < customSubmenuButton.length; iSub++ ) { // Add click event to the button to show ul.sub-menu customSubmenuButton[iSub].addEventListener( 'click', function () { // this refers to the current loop iteration of customSubmenuButton // nextElementSibling refers to the neighboring ul with .sub-menu class if ( this.nextElementSibling.className.indexOf( 'toggled-submenu' ) !== -1 ) { // if .sub-menu has .toggled-submenu class this.nextElementSibling.className = this.nextElementSibling.className.replace( ' toggled-submenu', '' ); // remove it this.nextElementSibling.setAttribute( 'aria-expanded', 'false' ); //console.log( 'button.' + this.className + ' is not toggled' ); } else { this.nextElementSibling.className += ' toggled-submenu'; // otherwise, add it this.nextElementSibling.setAttribute( 'aria-expanded', 'true' ); } //console.log( 'button.' + this.className + ' is toggled' ); } ); } // ends for loop // console.log( customSubmenuButton[iSub] ); }
I was having trouble getting the submenu to work as I wasn’t figuring out that I needed to use the this
keyword to target each dropdown. I came across the website in the comment above. On a side note, nodelist.js is something I might consider in a future project.
What this code does is assign a menu item with the class .menu-down-arrow (that is attached to a button) to the variable customSubmenuButton, loops through each instance like before, then adds an event listener for clicks. The this
keyword refers to customSubmenuButton. .nextElementSibling
refers to the item next in order, an unordered list. If the ul contains .toggled-submenu class replace it with an empty string, else add the string ‘ toggled-submenu’ to the class list. I added some console logs that aided me along while I was learning to show what is being toggled when you click the button.
In the previous example, I added the dropdown buttons if the screen size was 600 pixels or more. In the new version, I added the menu buttons in the addDesktopNavButtons function, like previously, but simply hid the menu itself and the sub-menu buttons when under 600px (in mobile). The menu and buttons are shown in screen sizes above 600px, but not the sub-menus.
The function must be placed after the addDesktopNavButtons function that dynamically adds the referenced buttons. Here is the code:
function loadInitMenuState() { // Loads the menu state depending on the screen size at the time of load windowWidth = window.innerWidth; var customSubmenuButton = document.querySelectorAll( '.main-navigation .menu-down-arrow' ); if ( typeof( menu ) != 'undefined' && menu != null ) { // if a menu exists on the page if ( windowWidth < 600 ) { menu.classList.add('hide'); for ( i = 0; i < customSubmenuButton.length; i++ ) { customSubmenuButton[i].classList.add('hide'); } } else { menu.classList.remove('hide'); for ( i = 0; i < customSubmenuButton.length; i++ ) { customSubmenuButton[i].classList.remove('hide'); } } } }
We also want this function to execute on page resize:
function themeNameResize() { var timeOut = setTimeout( function() { loadInitMenuState(); }, 250 ); } window.addEventListener( 'resize', themeNameResize );
The function executes on a delay of 250 milliseconds as the page is resizing to aid in performance.
The CSS
Finally, we need to add the CSS that is attached to the classes that hide and show the menus. Here, I will use pseudocode because each theme’s menu code might differ slightly. Replace .menu-container with the name of the container that holds your menu in your theme.
Let’s add the .hide
class that sends the element it is added to off the page. Hiding elements this way is better for accessibility than display: none
because screen readers can still see it.
.hide { position: absolute !important; left: -9999em; }
For Underscores, I had to make some more adjustments to the style sheet because by default, it gives you a hover based dropdown menu. If you are using Underscores, remove or comment out this snippet:
.main-navigation ul ul li:hover > ul, .main-navigation ul ul li.focus > ul { left: 100%; }
In your tablet size and up media query (or desktop size if you are using max-width), add:
.menu-container { position: relative; } @media screen and (min-width: 600px) { .menu-toggle { display: none; } .menu-container { min-height: pixel or em value; } /* If using flexbox */ .menu-container ul .menu-item-has-children, .menu-container ul .page_item_has_children { display: flex; } /* Hide sub menus */ .menu-container ul ul.sub-menu, .menu-container ul ul.children { display: block; position: absolute; left: -9999px; top: min-height of parent container; } .menu-container ul ul.sub-menu.toggled-submenu, .menu-container ul ul.children.toggled-submenu { left: 0; } /* Hide sub sub menus */ .menu-container ul ul ul { left: -9999em; top: 0; } .menu-container ul ul li > ul.sub-menu.toggled-submenu, .menu-container ul ul li.focus > ul.children.toggled-submenu { left: 25%; /* or any percentage from 0 to 100 horizontally */ } }
Finishing Up
A lot of the properties here, especially the JavaScript ones are meant to work in modern browsers. For Internet Explorer, that usually means version 10 and up. There are workarounds and polyfills for older browsers, though. There is a resources section below for all of the methods listed in this tutorial.
Below is a screenshot from The M.X. that shows how the JS only dropdown menu can look with some theme specific styling applied to it. Even though this theme does use jQuery for other purposes, if you want to include a menu and are not using jQuery, try these methods. Thanks for reading and see ya’ on the next tut.
Resources
From MDN:
- getElementById
- getElementsByTagName
- indexOf
- className
- replace
- insertAdjacentHTML
- HTMLElement.style
- querySelectorAll
- For Loop
Tutorials:
6 Comments
Trackbacks and Pingbacks
Trackback URL for this post: https://www.jasong-designs.com/2016/06/30/hiding-and-showing-a-wordpress-menu-ii-javascript-only-please/trackback/
-
[…] Update: There is now a follow up tutorial that describes how to create this same menu with pure JavaScript! Check it out. […]
Thanks, that’s amazing! ! Exactly what I am looking for.
I am relatively new to web design. May I have the navigation.js and CSS file as well?
Hello Bernd,
The theme that this code references is online at GitHub. From its online code, I can point you to enough code to set up a test case.
header.php:
Line 28 opens the header; Line 76 closes header.
Copy lines 72 to 75 for only the site navigation HTML.
navigation.js:
You can copy this entire file, as it opens and closes the menu when in mobile view.
The rest of the JavaScript that controls the menu is in the-mx-scripts.js:
Lines 338 through 405 are the three functions that hide and show the submenus. At the bottom of this file on lines 785 to 810 are the functions that load and change the menu state on resize. Create similar functions, but loading the three menu functions only. Plus loadInitMenuState for the resize function.
In style.css:
First needed is the class that hides the menu items.
.hide {
position: absolute !important;
left: -9999em;
}
Then, the code that styles the menu is under Menus/Header Buttons from lines 810 to 1153. Mostly everything with .menu and .main-navigation references the menu specifically.
When I have some time though, I am probably going to set up a Codepen for this tutorial in the future.
Thank you for this tutorial. I have been searching for this for a while and was very glad when I landed on your page.
I am new to wordpress development. I tried following the tutorial and somehow my code isn’t working. May I have the files you changed so that I can compare with my code and see where I went wrong.
I sent you the same files mentioned in the other comment. They should work. Let me know if there are any issues, as it has been awhile since I worked on the theme associated with this tutorial.
Can you send me the navigation.js and Css file which were adjusted in this post?
Thank you
I sent you an email with the files.