Navigation elements are always one of the most tricky to implement and take a lot of time to get right. This is true now so more than ever in that you have hundreds of various screen sizes. I remember discovering the ‘Suckerfish’1 dropdown in the early 2000s. And then in an attempt to further expand upon the proliferation of JavaScript as a means to deal with the problems of designing dropdown menus, there was Superfish and Son of Suckerfish. Now there is a slew of terminology for various implementations of a navigation menu like off-canvas, mega-menus, full screen, nav-bars, fly menus, etc. There are still in two primary flavors: vertical or horizontal. I tend to favor an entirely JavaScript free navigation even though I often have to accommodate the wishes of someone else. I think dropdown menus are confusing, annoying, and dysfunctional. I prefer to include the sub-navigation of a primary page as a secondary menu which can be done using a widget. I’ve built in two primary menu options and four locations into the theme which rely entirely on the default WordPress Menu manager.
Menu Locations
There are four menu locations in the theme:
- Top – Appears at the very top of the page which is designed for tertiary information like email addresses, phone numbers, or perhaps some key pages. I does not collapse on mobile, but shrinks in size and disappears on scroll.
- Primary – Appears in the masthead header in the site. It reappears on up scroll and is collapsed into the mobile. it is designed to handle the bulk of the site navigation.
- Bottom – Appears in the very bottom of the site. It is designed to display tertiary navigation items and collapses on mobile.
- Widget – This menu is put into a widget which can be added to one of the four widget areas of the site. It does not collapse on mobile and is limited to one level.
Multi Column Drop Down Menus
I think the best way to handle large menus are with multiple columns of navigation items. Some folks might use the term mega menu to describe this. I like to call them super menus. My solution for doing so is to simply add the classes directly to the default WordPress menu manager because this avoids any third party plugins or extra queries to rewrite the nav walker. Here are the steps to add multi-column drop-down menus using the WordPress Menu manager:
- Add menu items in Appearance -> Menus with each level of the structure representing an individual column.
- Under “Menu Structure”, each navigation item will dropdown to review a field entitled “CSS Classes (optional)”. Add the class “super“.
- Additional customizations to each column can be made using the classes.
- Icons can be added using the icon classes. “bi bi-arrow-down-circle”
- Column Heading classes can be added to the top column
- Section Dividers can be added using the class “dropdown divider”
Yamm
I’ve also included the Yamm CSS into the theme because it’s so tiny and so that additional menu customizations can be made using all of the Bootstrap elements like columns, forms, accordions, tables, and images. In order to add a yamm menu, you’ll need to wrap the wp_nav_menu function inside of the yamm HTML divs.
css/custom.scss
/*!
* Yamm! - Yet another megamenu for Bootstrap
* http://geedmo.github.com/yamm
* @geedmo - Licensed under the MIT license
*/
.yamm .collapse,
.yamm .dropup,
.yamm .dropdown {
position: static;
}
.yamm .container {
position: relative;
}
.yamm .dropdown-menu {
left: auto;
}
.yamm .yamm-content {
padding: 1rem;
}
.yamm .dropdown.yamm-fw .dropdown-menu,
.yamm .dropup.yamm-fw .dropdown-menu {
left: 0;
right: 0;
}
note: I understand that this isn’t necessarily as intuitive as having a couple of UI elements to control this options, it avoids re-writing the wheel or using and instead relies on heavily tested and maintained open source components. I’ve found that using the menu manger is a relatively rare occasion once a site is setup.
Mobile Menus
For mobile menus I prefer to use the default Bootstrap collapse element because of how widely it’s been tested. I do make two customizations to it because I find the hamburger top doesn’t really adhere to the sort of interaction suggested by Google’s Material Design2. I’ve also tried to avoid too much motion for the prefers-reduced-motion designed for accessibility. In lieu of the full page mobile menu, I add a considerable amount of padding to the bottom of the mobile menu so that it lends itself to being the strongest element on the page.
/css/custom.scss
.navbar-dark .navbar-toggler:focus {
box-shadow:0 0 5px 3px rgba(255, 255, 255, 0.5) !important;
border-color:none !important;
}
.navbar-toggler {
z-index:1000;
width: 30px;
height: 24px;
position: relative;
margin: 0 auto;
transform: rotate(0deg);
cursor: pointer;
border-radius: 1px;
}
.navbar-toggler span {
display: block;
position: absolute;
height: 3px;
width: 50%;
transform: rotate(0deg);
transition: 0.25s ease-in-out;
}
.navbar-toggler[aria-expanded=false] span:nth-child(even) {left: 50%;}
.navbar-toggler[aria-expanded=false] span:nth-child(odd) {left: 0px;}
.navbar-toggler[aria-expanded=false] span:nth-child(1),
.navbar-toggler[aria-expanded=false] span:nth-child(2) {top: 0px;}
.navbar-toggler[aria-expanded=false] span:nth-child(3),
.navbar-toggler[aria-expanded=false] span:nth-child(4) {top: 9px;}
.navbar-toggler[aria-expanded=false] span:nth-child(5),
.navbar-toggler[aria-expanded=false] span:nth-child(6) {top: 18px;}
.navbar-toggler[aria-expanded=true] span:nth-child(1),
.navbar-toggler[aria-expanded=true] span:nth-child(6) {transform: rotate(45deg);}
.navbar-toggler[aria-expanded=true] span:nth-child(2),
.navbar-toggler[aria-expanded=true] span:nth-child(5) {transform: rotate(-45deg);}
.navbar-toggler[aria-expanded=true] span:nth-child(1) {left: 5px;top: 7px;}
.navbar-toggler[aria-expanded=true] span:nth-child(2) {left: calc(50% - 5px);top: 7px;}
.navbar-toggler[aria-expanded=true] span:nth-child(3) {left: -50%;opacity: 0;}
.navbar-toggler[aria-expanded=true] span:nth-child(4) {left: 100%;opacity: 0;}
.navbar-toggler[aria-expanded=true] span:nth-child(5) {left: 5px;top: 12px;}
.navbar-toggler[aria-expanded=true] span:nth-child(6) {left: calc(50% - 5px);top: 12px;}
Multi Level Dropdowns
I strongly recommend against using multi-level drop-down menus, but I include a bit of code to handle it anyway for those that insist. One of the primary issues with three level mouseover dropdown menus is the positioning. If the primary dropdown is to the far left or right of the screen, then how do we position the third level dropdown. This bit of Javascript handles that determines if the third level goes off the screen and then positioning it to the other side of the primary dropdown.
js/init.js
$('ul.dropdown-menu > li.dropdown').on('mouseenter mouseleave', function (e) {
if ($('li.dropdown', this).length) {
var elm = $('ul:first', this);
var off = elm.offset();
var l = off.left;
var w = elm.width();
var docH = $('.container').height();
var docW = $('.container').width();
var isEntirelyVisible = (l + w <= docW);
if (!isEntirelyVisible) {
$('ul.dropdown-menu > li.dropdown ul').css({'right':'100%','left':''});
} else {
$('ul.dropdown-menu > li.dropdown ul').css({'right':'','left':'100%'});
}
}
});
Nav Walker
A Nav Walker is the file that interprets the list of navigation items from the WordPress menu manager. The WordPress Walker_Nav_Menu3 class extends the core walker class and uses the wp_nav_menu4 class to display navigation menus. In order to customize the HTML output, I use an open source walker called wp-bootstrap-nav-walker5 and I’ve customized it a bit to clean up the output.
inc/wp-nav-walker.php
/***********************************************************
// https://github.com/wp-bootstrap/wp-bootstrap-navwalker //
// License URI: http://www.gnu.org/licenses/gpl-3.0.txt //
// notes:
ln 97(201) add newline/tabs to <li> for cleaner output
ln 110(216) change $atts['data-bs-toggle'] = 'dropdown';
***********************************************************/
Menu Performance
Running through the nav walker and the queries need to build a large menu can be really taxing on the performance of the website. I recommend storing the large menus in a transient to stop the drain. The only drawback is that you lose the active page class. Here’s an example of the primary menu in this site stored as a transient and stored for 24 hours.
header.php
<?php
if (false === ($menu = get_transient('primary_menu'))){
ob_start();
wp_nav_menu(array(
'theme_location' => 'primary',
'container' => 'div',
'container_id' => 'navbar-primary',
'container_class' => 'justify-content-end collapse navbar-collapse',
'menu_id' => 'menu-primary',
'menu_class' => 'navbar-nav',
'depth' => 3,
'fallback_cb' => 'WP_Bootstrap_Navwalker::fallback',
'walker' => new WP_Bootstrap_Navwalker()
));
$menu = ob_get_clean();
set_transient( 'primary_menu', $menu, DAY_IN_SECONDS );
}
echo $menu;
?>
References:
- A List Apart – Suckerfish – https://alistapart.com/article/dropdowns/
- An adventure in CSS with column lists – https://haacked.com/archive/2018/12/03/css-column-list-adventure/
- yamm – https://github.com/geedmo/yamm
- Google Material Design – https://material.io/design
- Walker_Nav_Menu – https://developer.wordpress.org/reference/classes/walker_nav_menu/
- wp_nav_menu – https://developer.wordpress.org/reference/functions/wp_nav_menu/
- wp-bootstrap-nav-walker – https://github.com/wp-bootstrap/wp-bootstrap-navwalker
- An adventure in CSS with column lists – https://haacked.com/archive/2018/12/03/css-column-list-adventure/