Progressively-Enhanced Breadcrumb Navigation

A progressively-enhanced breadcrumb navigation pattern that allows for sibling pages to be easily accessible at every level via dropdown menus.

Breadcrumb navigation is an incredibly useful design pattern: it’s simple, recognizable, and it provides great usability. Traditionally, this pattern allows users the ability to retrace their path through the website, with each ‘crumb’ being a link to a parent page. In this post, we’ll walk though how to build a progressively-enhanced breadcrumb navigation that incorporates dropdown menus for each parent page link to allow easy access to sibling pages.

View Demo

Step 1: Baseline

By starting with baseline experience, we ensure that our component is available in some useable form to all users regardless of browser version, CSS feature support, Javascript feature support, etc.

Base Markup

We’ll start with the markup, which is essentially an unordered list and an additional nested list for each submenu. Let’s take a look:

<ul id="js-breadcrumb" class="breadcrumb">
  <li><a href="#">Home</a></li>
  <li>
    <a href="#" class="breadcrumb__toggle">Grandparent A</a>
    <ul>
      <li><a href="#">Grandparent A</a></li>
      <li><a href="#">Grandparent B</a></li>
    </ul>
  ...
  </li>
</ul>

This will provide us with a clean base to build on. Now let’s move on to base styling:

Base Styling

.breadcrumb {
  > li {
    display: inline-block;
    padding: $base-spacing/2 0;

    &:after {
      content: "/";
      display: inline-block;
      margin: 0 $base-spacing/2 0 $base-spacing/4;
    }
  }
}

.breadcrumb li {
  position: relative;

  ul {
    position: absolute;
    top: 100%;
    left: 0;
    left: -9999px;
    z-index: 2;
    background-color: white;

    a {
      display: block;
      white-space: nowrap;
      padding: $base-spacing/2 $base-spacing;
    }
  }
}

*Note: For brevity, I am excluding some styles and showing only the rules that are essential.

Our base styling for the breadcrumb nav is pretty straight-forward. The basic gists is we align our top-level list items horizontally via display: inline-block and ensure that we apply position: relative to give our nested sub-menus a context. Next, we place the sub-menus under its parent list item with some absolute positioning and hide it off-canvas. For our breadcrumb dividers, we are using generated content (:after) on the top-level list item for some visual separation. This should do it for our baseline breadcrumb nav.

*Note: An important thing to note here is the use of the child selector (‘>’), which helps to ensure we are selecting the correct list or list-item. To learn more about this helpful CSS selector, check out the Mozilla Developer Docs.

Step 2: Reveal Sub-Menu on Click or Tap

The next step is to allow our sub-menus to be toggled by users that can support the necessary Javascript. Let’s dive in and tackle this one piece at a time:

Base Javascript

We’ll start by testing if the users browser understands the .addEventListener() method — if it does, then it can proceed. If it doesn’t, then it is returned and the rest of the Javascript isn’t read. This means that legacy browsers like IE8 will fallback to our baseline breadcrumb navigation. Additionally, let’s add an enhancement class to the document element, which we’ll use later for adding arrows to each dropdown.

(function (win, doc) {
    'use strict';

    // Return if addEventListener isn't supported
    if (!win.addEventListener) {
        return;
    }

    // Add Enhancement Class
    doc.documentElement.className += ' ' + enhanceclass;

}(this, this.document));

*Note: This method of feature testing is known as ‘cutting the mustard’, which was popularized by the developers over at BBC News.

Toggling the Active State

Provided that the users browser cuts the mustard, we can then listen for click events on elements with the ‘breadcrumb__toggle‘ class, which will fire the breadcrumbListener() method. This method will prevent the default behavior of the link and then toggle our activated class ('is-toggled’). By targeting this class in our styles, we can reveal the sub-menus on the ‘crumb’ that has been selected:

(function (win, doc) {
    'use strict';

    // Store Variables
    var enhanceclass = 'cutsthemustard',
        breadcrumbNav = document.getElementById('js-breadcrumb'),
        breadcrumbToggleClass = 'breadcrumb__toggle',
        breadcrumbHoverClass = 'is-toggled';

    // Return if addEventListener isn't supported
    if (!win.addEventListener) {
        return;
    }

    // Toggle className Helper Function
    var toggleClassName = function (element, toggleClass) {
        var reg = new RegExp('(\\s|^)' + toggleClass + '(\\s|$)');
        if (!element.className.match(reg)) {
            element.className += ' ' + toggleClass;
        } else {
            element.className = element.className.replace(reg, '');
        }
    };

    // Process event on Breadcrumb Nav
    var breadcrumbListener = function (ev) {
        ev = ev || win.event;
        var target = ev.target || ev.srcElement;
        if (target.className.indexOf(breadcrumbToggleClass) !== -1) {
            ev.preventDefault();
            toggleClassName(target.parentNode, breadcrumbHoverClass);
        }
    };

    // Add Enhancement Class
    doc.documentElement.className += ' ' + enhanceclass;

    // Listen for clicks (or taps) in Breadcrumb Navigation
    breadcrumbNav.addEventListener('click', breadcrumbListener, false);

}(this, this.document));
.breadcrumb li.is-toggled ul {
  left: 0;
}

We also want to be sure to indicate to the user that the crumbs are able to be toggled. To do this, we’ll leverage the class that was added to our element once the browser cut the mustard and add arrows after each toggle element.

.cutsthemustard .breadcrumb__toggle:after {
  content: "";
  display: inline-block;
  vertical-align: middle;
  width: 0;
  height: 0;
  margin-left: $base-spacing/3;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid lighten($base-accent-color, 35%);
}

Things are looking good so far: we have a baseline breadcrumb navigation for users in less capable browsers, and a toggle-enabled breadcrumb navigation which reveals sub-menus for everyone else. What about making things a bit easier for the user and reveal those helpful sub-menus on :hover instead?

Step 3: Reveal Sub-Menu on :hover

The final enhancement is to reveal our sub-menus when the parent list item is moused over, saving the user some clicks. But there is a challenge here: how do we determine if the user is using a mouse versus if they are on a touch-enabled device, and therefore cannot detect mouse events? You would think that it is as simple as testing if the device is touch-enabled; unfortunately, there is no reliable way to detect touchscreen.

Another approach could be to just reveal the sub-menus with the :hover CSS selector. While this will work for devices that do understand mouse events, it becomes problematic on devices that emulate :hover (which is basically all touch device browsers). This is because emulated hovering has some ugly quirks, including hovered elements staying in the :hover state even after the user stops interacting with it. This would not be an ideal side-effect.

But, what if we could test if :hover is properly supported and provide our enhancement only to those devices that pass? With CSS level 4 media queries we have interaction media features, which includes the ‘hover’ media query. Which provides us a reliable way to query the user’s ability to hover over elements on the page. While the support is not perfect, it is enough for us to confidently implement a solution for browsers that do support it, and have our toggle to reveal fallback there for those browsers that don’t support it. Progressive enhancement FTW.

Query for Hover

Our first step in this last enhancement is to add the hover media query to our styling. This can be applied like a normal media query, but we are passing in hover in the query string. Within this media query, we are simply revealing our sub-menus on each parent list item once they are moused over.

@media (hover) {
  .breadcrumb li:hover ul,
  .breadcrumb li:focus ul {
      left: 0;
  }
}

Final Touch

Everything is working great so far. There is just one thing: the top crumb link is being intercepted by our toggle method for users that can properly support hover. This not only prevents the link within from working, but also toggles our menu open if clicked. We can easily fix this by telling our Javascript that hover is supported, and then prevent the event listener on these top level links from being activated. To do this, we can use a pseudo-element on our breadcrumb that is hidden but will have a keyword that can be read in Javascript.

.breadcrumb:after {
  display: none;

  @media (hover) {
      content: 'hover';
  }
}

Next, we will write a method that can check this value and see if it matches. In our case, we are checking if the keyword equals ‘hover’. If it does, we can return before the event listener is added to our breadcrumb toggle elements:

(function (win, doc) {
    'use strict';

    // Return if addEventListener isn't supported
    if (!win.addEventListener) {
        return;
    }

    // Store Variables
    var enhanceclass = 'cutsthemustard',
          breadcrumbNav = document.getElementById('js-breadcrumb'),
          breadcrumbToggleClass = 'breadcrumb__toggle',
          breadcrumbHoverClass = 'is-toggled';

    // Remove string quotes to normalize browser behavior
    var removeQuotes = function(string) {
        if (typeof string === 'string' || string instanceof String) {
            string = string.replace(/^['"]+|\s+|\\|(;\s?})+|['"]$/g, '');
        }
        return string;
    };

    // Get value of body generated content
    var checkBreadcrumbMedia = function() {
        var media = window.getComputedStyle(breadcrumbNav,':after').getPropertyValue('content');
        return removeQuotes(media);
    };

    // Toggle className Helper Function
    var toggleClassName = function (element, toggleClass) {
        var reg = new RegExp('(\\s|^)' + toggleClass + '(\\s|$)');
        if (!element.className.match(reg)) {
            element.className += ' ' + toggleClass;
        } else {
            element.className = element.className.replace(reg, '');
        }
    };

   // Process event on Breadcrumb Nav
    var breadcrumbListener = function (ev) {
        ev = ev || win.event;
        var target = ev.target || ev.srcElement;
        if (target.className.indexOf(breadcrumbToggleClass) !== -1) {
            ev.preventDefault();
            toggleClassName(target.parentNode, breadcrumbHoverClass);
        }
    };

    // Add Enhancement Class
    doc.documentElement.className += ' ' + enhanceclass;

    // Fire checkMedia to get value
    checkBreadcrumbMedia();

    /**
     * Do nothing if pointing device can relay a hover state
     * Else, attach event listener to breadcrumb
     */
    if ( checkBreadcrumbMedia() === "hover" ) {
      return;
    } else {
        breadcrumbNav.addEventListener('click', breadcrumbListener, false);
    }

}(this, this.document));

*Note: The removeQuotes() method is necessary here in order to normalize what our output of the checkBreadcrumbMedia() method returns. This is due to the fact that some browsers return double-quotes, while others return single-quotes.

All done! Here is the final result:

See the Pen Dropdown Breadcrumb Navigation by Jon Yablonski (@jmy1138) on CodePen.1331


Progressive-enhancement is a great way to add functionality to your components in layers that can build on top of each other, all the while providing fallbacks to browsers that don’t support the necessary features. We have used this methodology to create a breadcrumb navigation that leverages all the usability of the pattern, and then enhances it with additional functionality. In this case, we are allowing for sibling pages to be easily accessible via dropdown menus at every level, but ensuring that a baseline experience is available for less capable browsers.

Further Reading