Collapsing Site Menu (with Sub-menus) on Small Screens in WordPress

Most site designs include the main navigation menu in the header at the top of the html. Collapsing this menu on smaller devices saves critical top-of-page real estate so users can get to their content as quickly as possible without having to scroll past a bunch of links on every single page.

The approach I’ll take here is adapted from the samples in Brad Frost’s excellent “This is Responsive” pattern collection.

More specifically, I’ll work from his basic Multi-Toggle navigation.

The basic function of the above pattern is this:
At desktop widths, submenus appear as the usual drop-downs on hover. (Note: hover is not a reliable feature, and touch devices don’t handle it consistently. Be sure to give some consideration to accessibility and touch when implementing your wide-screen submenus. (One method is described here.)
On narrower screens, the whole site menu is hidden behind a “menu” link. When clicked, the top-level links are revealed, and those with sub-menus are indicated by a “+” icon. When clicked, those top-level links reveal their submenus and the icon changes to a “-”. When clicked again, the sub-menu disappears and the “+” icon returns.

I will adjust some of the styling and behavior for my own purposes as I go.

Duplicate Footer Menu / No-Javascript Fallback

Thinking in the spirit of “mobile first” and “progressive enhancement”, we will first consider the least capable devices: phones with no javascript support.

Brad’s pattern does use javascript, so it won’t work for those devices.

Since phones and other small screens involve a lot of scrolling, I think it’s a nice gesture to add a duplicate nav menu at the bottom of the screen so users can easily move on to other parts of the site without having to scroll back to the top again.

I’ll add this extra menu at the bottom, and on small devices without javascript support, I’ll hide the top menu entirely (using “display: none”) but include a Site Menu link that jumps to this footer menu. I’ll also include a “Back to Top” link to give users an easy way to return.

Some designers use this jump-to-the-foot approach for all small-screen users and it can be a fairly elegant solution if handled well, but I personally prefer not to send users flying back and forth over the document if I can help it, so I’ll just use it for the non-javascript fallback.

To add the extra menu, I just copy the php code from header.php that inserts the menu, something like:

<?php wp_nav_menu( array( 'container_class' => 'menu-header', 'theme_location' => 'primary' ) ); ?>

In this case I would just change the “container_class” to “menu-footer” and paste it into my footer element in footer.php:

<?php wp_nav_menu( array( 'container_class' => 'menu-footer', 'theme_location' => 'primary' ) ); ?>

In my CSS, I will add “display: none;” to this footer menu in my media query for desktop-width screens, because I don’t believe those users will find it that helpful. For example:

@media only screen and (min-width: 48em) {
  .menu-footer {
    display: none;
  }
}

Note: I admit that by including html for both menus but hiding one of them with “display: none” I am serving extra, unused html both to non-javascript phones and to desktop browsers. But I believe the code bloat here is not that bad and is outweighed by the benefits to the majority of smartphone users.

Before I add the jump links for no-javascript devices, I will style the menus in CSS using media queries so that the small-screen menus are attractive and show a stack of full-width, “display: block” links with no floats (assuming the wider-screen nav is a horizontal menu using floated or inline-block links). Brad Frost’s pattern gives examples of the difference.

Now I add the “Site Menu” link immediately above each menu. The top one gives us the “jump-to-bottom” link for our no-javascript friends, and later on BOTH of them will serve as toggle links to display and hide their respective menus.

Here’s the html for the link:

<a class="menu-link" href="#foot-menu">Site Menu</a>

First, add an id of “foot-menu” to the element that contains the footer menu. Note that unlike in Brad’s version, the href is pointing to the menu in the footer.

When we implement the javascript later on, it is critical that the “Site Menu” links should be siblings of the menu ul’s and appear immediately before them in the html. To accomplish this in WordPress, we’ll use the “items_wrap” parameter for “wp_nav_menu” ( http://codex.wordpress.org/Function_Reference/wp_nav_menu )

Find and edit the wp_nav_menu function calls for both header.php and footer.php.
These could be directly in the templates, or in a functions file (in the Bones starter theme, they are in library/bones.php)
Make sure they have different ‘container_class’ parameters (i.e., ‘menu-header’ and ‘menu-footer’ as the CSS uses in this example)
Add an ‘items_wrap’ parameter including the “Site Menu” link:

'items_wrap' => '<a class="menu-link" href="#foot-menu">Site Menu</a><ul class="%2$s" id="%1$s">%3$s</ul>'

Then I add the “Back to Top” link immediately above the footer menu:

<a class="top-link" href="#header">&#9650; Back to Top of Page</a>

I included

&#9650;

(or ▲) at the start of the link text to display an upwards arrow. Later I’ll be adding this and the downward arrow:

&#9660;

(or ▼) to the links revealing drop-down menus.

I style all three of these new links using CSS in whatever way is suitable for the site.

Then I hide .menu-link on wide screens just like I did the footer menu (.top-link should already hidden along with the footer menu)

@media only screen and (min-width: 48em) {
  .menu-footer,
  .menu-link {
    display: none;
  }
}

Lastly, I’ll hide the header menu and the “site menu” footer link on small screens without javascript, and hide the “back to top” link on all screens WITH javascript. Since I’m using Modernizr, I will use its .no-js class to identify browsers without javascript, and .js for those with it (alternatively, you can manually add a .no-js class and swap it out with a .js class using javascript, but I won’t go into the details on that here).

Outside of my media queries I will add something like

.no-js .menu-header,
.no-js #foot-menu .menu-link,
.js .top-link {
  display: none;
}

and then I’ll reveal the top menu again inside my desktop media query, since my CSS drop-downs don’t require javascript.

@media only screen and (min-width: 48em) {
  .no-js .menu-header {
    display: block;
  }
}

Now all the html is in place and our non-javascript phones are taken care of.

CSS for toggling the Site Menu and Submenus

Brad’s example applies his styles for the main menu transition to the element wrapping around the menu, and the submenu styles to the submenu ul itself. We’re going to edit his javascript later, and so accordingly we’re going to apply OUR main menu transition styles to the ul inside of that wrapper, and not the wrapper itself.

Apply transitions to the elements wrapping around each menu ul, and the sub-menus inside them.

.menu-header > ul,
.menu-footer > ul,
.menu-header > ul ul,
.menu-footer > ul ul {
  -webkit-transition: all 0.3s ease-out;
  -moz-transition: all 0.3s ease-out;
  -ms-transition: all 0.3s ease-out;
  -o-transition: all 0.3s ease-out;
  transition: all 0.3s ease-out;
}

Hide overflow and set max-height to zero for both menus (and submenus) when javascript is available. Max-height is the attribute that will be animated (height is not animatable, for some reason)

.js .menu-header > ul,
.js .menu-footer > ul,
.js .menu-header > ul ul,
.js .menu-footer > ul ul {
  overflow: hidden;
  max-height: 0;
}

Undo these settings at desktop widths.

@media only screen and (min-width: 48em) {
  .js .menu-header > ul,
  .js .menu-footer > ul,
  .js .menu-header > ul ul,
  .js .menu-footer > ul ul {
    max-height: none;
    overflow: visible;
  }
}

With javascript, we will add and remove a class of “active” to reveal and hide the menus. The .active class will be added using javascript, so we don’t need to include the .js test class.

Set sufficient max-height to the .active class menus to reveal all of their links, but not too much, or there will be a delay in the transitional animation as it passes over the extra empty space.

.menu-header > ul.active,
.menu-footer > ul.active,
.menu-header > ul ul.active,
.menu-footer > ul ul.active {
  max-height: 10em;
}

In order to target the top-level links with submenus, we will give them a class of .has-subnav by adding the following to functions.php (found at http://stackoverflow.com/questions/8448978/wordpress-how-do-i-know-if-a-menu-item-has-children ):

function menu_set_dropdown( $sorted_menu_items, $args ) {
$last_top = 0;
foreach ( $sorted_menu_items as $key => $obj ) {
// is it a top level item?
if ( 0 == $obj->menu_item_parent ) {
// set the key of the parent
$last_top = $key;
} else {
$sorted_menu_items[$last_top]->classes['has-subnav'] = 'has-subnav';
}
}
return $sorted_menu_items;
}
add_filter( 'wp_nav_menu_objects', 'menu_set_dropdown', 10, 2 );

Add down arrow to .menu-link and top-level links with submenus, switching to an up arrow once the submenu has been revealed. According to Chris Coyier, we have to use the ASCII number for the arrows instead of the html entities:

Down arrow: &#9660; and Up arrow: &#9650; 

The ASCII number for the down arrow is “\25BC”, and themup arrow is “\25B2”. Also thanks to Coyier, this listing of geometric shapes can be converted to ASCII using this converter.

.menu-link:before,
.menu li.has-subnav > a:before {
  content: '\25BC';
  font-size: 0.75em;
  padding: 0.25em 0.5em 0.25em 0;
}
.menu-link.active:before,
.menu li.has-subnav > a.active:before {
  content: "\25B2";
}

The content here is what’s critical. The font-size and padding is theme-specific and should be adjusted to what looks good. Additionally, depending on the design, “:before” can change to “:after” as Brad has it, and the icons can be whatever works for the context.

And the CSS is done!

Add the Javascript to enable the toggling

Here’s the script from Brad’s example:

$(document).ready(function() {

  $('body').addClass('js');
     var $menu = $('#menu'),
     $menulink = $('.menu-link'),
  $menuTrigger = $('.has-subnav > a');

  $menulink.click(function(e) {
    e.preventDefault();
    $menulink.toggleClass('active');
    $menu.toggleClass('active');
  });

  $menuTrigger.click(function(e) {
    e.preventDefault();
    var $this = $(this);
    $this.toggleClass('active').next('ul').toggleClass('active');
  });

});

Since I’m using Modernizr, I will remove “$(‘body’).addClass(‘js’);”

The next section sets up the variables we will need:

var $menu = $('#menu'),
$menulink = $('.menu-link'),
$menuTrigger = $('.has-subnav > a');

In Brad’s example, there is only one menu, while we have two. This requires us to make some changes.

“$menu = $(‘#menu’)” selects Brad’s menu, which is identified with an id. Our two menus are idenitifed with classes (.menu-header and .menu-footer). Thankfully, we will rework the script so we don’t need this variable at all, so we can remove “$menu = $(‘#menu’)” completely:

var $menulink = $('.menu-link'),
$menuTrigger = $('.has-subnav > a');

In Brad’s case, “$menulink = $(‘.menu-link’)” applies to only one link on the page, but we have two identical links controlling two almost identical menus, so in our case “$menulink = $(‘.menu-link’)” targets both of our links.

This is not a bad thing, but the script in its current form will cause us some problems. Let’s look at the next part of Brad’s script:

$menulink.click(function(e) {
  e.preventDefault();
  $menulink.toggleClass('active');
  $menu.toggleClass('active');
});

“$menulink.click(function(e)” activates when either of our “Site Menu” links get clicked. This is fine.

“e.preventDefault();” stops the link from jumping to “#footer-menu”, so we stay put and just reveal the menu. The only problem here is if we want our top-level links to operate on large screens, in which case we need to wrap the “preventDefault” line in an if statement limiting it to our small screen width, like so:

if( document.documentElement.clientWidth < 570 ) {
  e.preventDefault();
}

In this case, “570” is the width in pixels where the menu changes.

“$menulink.toggleClass(‘active’);” adds and removes the “active” class on everything that matches the variable, so any time we click either “Site Menu” link, this script will activate BOTH of our links, and that’s not what we want.

“$menu.toggleClass(‘active’);” is using the “$menu” variable, which we got rid of, so it’s not doing anything at all!

Thankfully, the next bit of the script gives us a model to follow to make this work:

$menuTrigger.click(function(e) {
  e.preventDefault();
  var $this = $(this);
  $this.toggleClass('active').next('ul').toggleClass('active');
});

“var $this = $(this);” gives us a variable so that we’re only working with the link we actually clicked, and not every single link included in the variable.

“$this.toggleClass(‘active’).next(‘ul’).toggleClass(‘active’);” can replace both of the last two lines of the previous section and is more specific. It adds an removes the “active” class ONLY on the link we click, PLUS it does the same to the “ul” element that immediately follows the link we clicked (note that “next(‘ul’)” only applies to a ul that is the next sibling element. It does not apply to the nearest ul appearing farther down in the html). This works great for revealing and hiding the submenus, and thankfully it also works great for revealing and hiding our two main site menus, which are also “ul” elements that immediately come after their links. We made sure of that earlier by using “wp_nav_menu” to add the links.

Since this function works for both Site Menu and submenu links, why not combine them into one variable?

So here’s our final script:

var $menuTrigger = $('.menu-link, .has-subnav > a');

$menuTrigger.click(function(e) {
  if( document.documentElement.clientWidth < 570 ) {
    e.preventDefault();
  }
  var $this = $(this);
  $this.toggleClass('active').next('ul').toggleClass('active');
});

Now our menu should be working as intended.