Since WordPress introduced native support for navigation menus in version 3.0, we can find them on almost every website. Undoubtedly they are popular, intuitive and easy to use for most of your website visitors. But at the same time a mouse-hover dropdown navigation menu anticipates that your visitors will sit in front of the screen and use a specific input device to interact with your site: a mouse. Unplug the mouse (or disable your trackpad if that's how you control it) and the second most vital part of every website (after the actual content) vanishes. We can do better than that! This article will explain a small but important step making your WordPress site a better place for all people with minimal effort.

We will take a look at how to expand the behavior and make dropdown navigation in your WordPress site richer in terms of usability and independent of the specific input devices your visitors tend to use.

The term browser in this article is used more broadly than you might have realized so far. In addition to the major desktop browsers you know already, think about screen readers or any other device capable of reading a HTML document.

WAI-ARIA

WAI-ARIA is an abbreviation for the Web Accessibility Initiative – Accessible Rich Internet Applications and if you follow a few simple rules you can develop a site that is much more user friendly for people who use different devices and software to browse the web. Take this example: around 7% of adults are blind or have serious difficulty seeing, and in contrast, less than 3% of people use IE 6-8 to access the web. Many developers are still forgetting about the former and struggling to support the latter, despite the fact there are more (semi-)blind people and it is easier and more important to provide a solution for them than forcing a HTML5 site to function flawlessly in IE 6. This is why you should seriously consider using WAI-ARIA specs.

Let's make things clear. WAI-ARIA attributes will not change the layout or style of your pages but they will make your site richer with information for screen readers and other similar assistive technologies.

There are two parts of the WAI-ARIA specification that we will take a closer look at: landmark roles and ARIA states.

Landmark roles

ARIA landmark roles are attributes in HTML that can drastically improve the accessibility of your website. Their main purpose is to give screen readers an option for fast access to the main areas of a page. Currently there are 8 roles available:

  • role="banner" - Used for the header of the website. Inside we will usually find the logo, search, navigation, etc. It is recommended to use only one banner role per page.
  • role="navigation" - Used for links and menus for navigation through the site. We will use this one later.
  • role="main" - This is for our main content area.
  • role="complementary" - This role is used in a sidebar. You can use it more than once.
  • role="contentinfo" - Used for the footer area. Just like banner role, contentinfo should be used just once on a page.
  • role="search" - WordPress uses this role by default but if you make a custom search form you should add this role.
  • role="form" - This should be used if any kind of form is present on the page.
  • role="application" - If a page has some kind of unique software, use this role.
Landmark roles
Landmark roles in our BuildPress theme

ARIA states

ARIA states are specific pieces of information that usually change after some kind of action has been taken by the user. We will see how all of this works in the dropdown menu example below.

All global states and properties can be found in this link.

Making your WP navigation menu accessible

Time to get your hands dirty. Let's take a look at how to implement the WAI-ARIA landmark roles and states properly for the dropdown menus in WordPress.

Menus are inserted into theme templates using the wp_nav_menu() function. For compatibility it is always best to rely on the native functions available in the WordPress core. Fortunately, wp_nav_menu() is easily configurable, so we can provide the right parameters to the function to make our dropdown navigation friendly for assistive technologies and keyboard navigation.

PHP, HTML and WordPress

Let's take a look at this code example from the header.php file from one of our themes:

<nav class="collapse  navbar-collapse" role="navigation" aria-label="<?php _e( 'Main Menu', 'textdomain' ); ?>">
  <?php
    if ( has_nav_menu( 'main-menu' ) ) {
      wp_nav_menu( array(
        'theme_location' => 'main-menu',
        'container'      => false,
        'menu_class'     => 'main-navigation',
        'walker'         => new Aria_Walker_Nav_Menu(),
        'items_wrap'     => '<ul id="%1$s" class="%2$s" role="menubar">%3$s</ul>',
      ) );
    }
  ?>
</nav>

First, we have a <nav> wrapper around the function call. <nav> is an HTML5 element for grouping navigation links together, but it needs some more context, namely these two additional attributes:

  • role="navigation" is the landmark role telling the browser there will be a menu inside, so the screen reader can jump straight to this section of the page if the user decides to.
  • aria-label="<?php _e( 'Main Menu', 'textdomain' ); ?>" tells the browser the title of the current menu. Other examples of labels are Sidebar menu, Footer menu or even Table of contents. I used the _e() i18n function here to output the actual text, so the menu label can be translated into languages other than English -- another important aspect for accessibility.

What about the configuration parameters we pass to wp_nav_menu()?

  • 'container' => false prevents our menu wrapping to an additional <div>.
  • 'walker' => new Aria_Walker_Nav_Menu() is the most crucial parameter. It tells the menu to use our custom menu walker and not the default one when iterating through menu items. This is important, as the Aria_Walker_Nav_Menu() walker outputs slightly modified HTML markup, with WAI-ARIA attributes properly implemented. Magic!
  • 'items_wrap' => '<ul id="%1$s" class="%2$s" role="menubar">%3$s</ul>' is the outermost <ul> container of our menu. By default this parameter lacks the attribute role="menubar", which represent the horizontal menu, hence we fix that with custom code (don't mind strange strings like %1$s, they are replaced dynamically upon PHP execution).

Of course, when wp_nav_menu() is called, the class Aria_Walker_Nav_Menu should exist already in the global space. Download this file and place it somewhere in your theme folder and then include it in your functions.php:

require_once "path/to/aria-walker-nav-menu.php";

This walker aims to resemble the default WP nav walker as much as possible, the only difference being the accessibility improvements; role="menu" is added to the output of the method start_lvl and several other attributes are added to start_el: role="menuitem" for all <li> elements and aria-haspopup="true" aria-expanded="false" tabindex="0" for <li>s with submenus.

Explanation? aria-haspopup indicates whether <li> controls the popup-like behavior of its descendants -- which a dropdown menu obviously is. aria-expanded describes the state of the "popup" and defaults to false (invisible/closed dropdown menu). Lastly tabindex="0" allows the element to reach the focus when the site is navigated with a keyboard -- by default only links and form elements can reach the focus.

JavaScript

Because accessibility should be a fundamental part of every website, we want to avoid using JS as much as possible. It would actually be easier to add the attributes mentioned above via JS, but that would also be more error prone than having them in the source HTML -- JS can break, HTML and CSS can't.

But, nevertheless, we cannot avoid using JS when we need to update the ARIA states. That's why we need to enqueue the following piece of JavaScript code:

jQuery( function ( $ ) {
  // Focus styles for menus when using keyboard navigation

  // Properly update the ARIA states on focus (keyboard) and mouse over events
  $( '[role="menubar"]' ).on( 'focus.aria  mouseenter.aria', '[aria-haspopup="true"]', function ( ev ) {
    $( ev.currentTarget ).attr( 'aria-expanded', true );
  } );

  // Properly update the ARIA states on blur (keyboard) and mouse out events
  $( '[role="menubar"]' ).on( 'blur.aria  mouseleave.aria', '[aria-haspopup="true"]', function ( ev ) {
    $( ev.currentTarget ).attr( 'aria-expanded', false );
  } );
} );

All this does is that once any element with the aria-haspopup="true" attribute triggers focus or blur events, the corresponding aria-expanded state is updated to true or false, respectively.

Enqueue this file with the following PHP code inside functions.php:

add_action( 'wp_enqueue_scripts', function() {
  wp_enqueue_script( 'textdomain-wai-aria', get_template_directory_uri() . '/path/to/wai-aria.js', array( 'jquery' ), null );
} );

And you're done! Your site navigation makes much more sense now to screen readers and other similar devices. But we are not totally finished yet.

CSS

The last thing we have to do is to make the navigation menus keyboard-accessible. When someone is using the TAB key, they should be able to navigate into submenus. This is not the case by default, but we can easily fix this.

For the mouse-hover dropdown menus, you probably already have something like this in your SCSS:

ul.main-menu {
  li.menu-item-has-children {
    ul.dropdown-menu {
      position: absolute;
      left: -9999px;
      ...
    }
  }

  li.menu-item-has-children:hover {
    ul.dropdown-menu {
      left: 0;
      top: 100%;
      ...
    }
  }
}

All you have to do is to add the [aria-expanded="true"] CSS selector next to all selectors that have the :hover state defined already. An altered example from above would then look like this:

ul.main-menu {
  li.menu-item-has-children {
    ul.dropdown-menu {
      position: absolute;
      left: -9999px;
      ...
    }
  }

  li.menu-item-has-children:hover,
  li.menu-item-has-children[aria-expanded="true"] {
    ul.dropdown-menu {
      left: 0;
      top: 100%;
      ...
    }
  }
}

Conclusion

Mastering WAI-ARIA is not very easy and it took us several days plowing through the official specification, recommendations and use cases scattered all over the web. It is hard to find the right information, but once you know what to use, the implementation itself is not hard at all.

That's why we decided to create a generic solution and make it open source. The code above is available as a Composer package on GitHub and licensed under GPLv2, so everyone can include it easily in any WP project, free or paid.

Let me finish this article with a quote that changed my view about accessibility from A List Apart:

We need to change the way we talk about accessibility. Most people are taught that “web accessibility means that people with disabilities can use the Web”—the official definition from the W3C. This is wrong. Web accessibility means that people can use the web.

-- Anne Gibson

Quality: The Codeable Differene

  • webmandesign

    Wonderful article! Just what I needed. I love the solutions with minimum JS.

    The WP menu markup can be changed via filters too (the “walker_nav_menu_start_el” and “wp_nav_menu_items” namely), which is preferable option (by me) as I do not have to copy the whole nav menu walker markup from WP actually.

    Thank you for this!

    • Hi! Thanks for commenting and excuse me for being silly and replying 9 months later. I didn’t check back this article much. Next time ping me on twitter: @primozcigler.

      I cannot recall the exact reason, but I think that these two functions are too deep down and there is not enough context available or something like that. I am sure we went with as little changes in code as possible. If you believe I’m wrong, please open a PR on github and I will be happy to merge it.

      • webmandesign

        Hi Primoz,

        No worries. I understand that my solution won’t work in all cases actually. I wish WordPress added some more control via filters into navigation walker.

        Fan of your work! Keep it up!

        Regards,

        Oliver

  • John

    Hi Primoz, I’ve just been going through the same process over the past few days. Learning, researching and testing existing solutions and this one is basically where I found myself settling. But I still don’t feel right re-writing the whole start_el function… I think at least for the haspopup attribute, it should be in core as it doesn’t rely on the theme included any javascript. What do you think?

    • Hi John. Thanks for commenting and excuse me for being silly and replying 9 months later. I didn’t check back this article much. Next time ping me on twitter: @primozcigler.

      To your questions: I admit that I do not remember correctly anymore how we converged to this final solution, but we’ve had quite some discussion and issues closed after I wrote this blogpost and I think the code will not be changing anymore.

  • icodethings

    What are the *.aria event names on the jQuery event handlers? Are they event names defined by aria? Is it a jQuery syntax? I’ve never seen it used like that before…

  • Wendy

    Hi, I followed all the steps above to implement the accessibilty menu, but it didn’t work.