Freebie: Bootstrap navbar with progressive collapse nav

If you've ever been to BBC.com you might have noticed their very neat main navigation menu at the top progressively collapses as the screen width changes. We took this inspiration and decided to see if we could make something similar with a Bootstrap navbar.

icon icon

The Inspiration & Motivation

As we mentioned, we took inspiration for this free Bootstrap download from BBC.com. Let's take a quick look at the functionality in action so you know what we're talking about:

Pretty slick eh? BBC.com is full of nice responsive touches so well worth a closer look (after you're done here of course).

Bootstrap's collapse functionality is great but changing the width at which it collapses isn't very easy in Bootstrap 3 (although Bootstrap 4 makes it blindingly easy) so that's a big motivator in having this functionality on your Bootstrap navbar.


Promote your skills, products or services, write for us! We're currently looking for contributors for the site, find out how to contribute to BootBites.com.

The Code

HTML Markup

The Trigger

Like in previous freebies we're triggering our collapse nav functionality using a data-toggle attribute on the element we want to act on. We're calling this functionality "collapse-nav" so our data attribute looks like so: data-toggle="collapse-nav".

We've only tested this on Bootstrap navbar nav list & breadcrumb list elements but it should work on other list types, although you might need to implement some extra styling. For this example we'll be using a Bootstrap navbar nav list element.


The Target

Our "collapse-nav" list also needs a "target" which is where the nav items that don't fit in the list get put as the screen width changes. We'll call this our "target" and define it on the data-toggle="collapse-nav" element by adding data-target="JQUERY-SELECTOR-OF-TARGET" ie. data-target="#more-menu-1". You can use any jQuery selector for this although we recommend using an ID instead of a class to the target is unique.

As mention we've only test this with Bootstrap navbar nav lists so we also need to wrap our list with <nav class="navbar navbar-default">.....</nav> as the Javascript needs a wrapper to read the available width from.

Our final mark up looks something like this:

<nav class="navbar navbar-default">
  <!-- Trigger element -->
  <ul class="nav navbar-nav" data-toggle="collapse-nav" data-target="#more-menu-1">
    <li class="sticky"><a class="navbar-brand" href="#">Brand</a></li>
    <li><a href="#">Link 1</a></li>  
    <li><a href="#">Link 2</a></li>  
    <li><a href="#">Link 3</a></li>  
    <li><a href="#">Link 4</a></li>  
    <li class="sticky"><a href="#">Sticky Link</a></li>  
    <li><a href="#">Link 6</a></li>  
    <li><a href="#">Link 7</a></li>  
    <li><a href="#">Link 8</a></li>  
    <li><a href="#">Link 9</a></li>  
    <li><a href="#">Link 10</a></li>  
    <li><a href="#">Link 11</a></li>  
    <li><a href="#">Link 12</a></li>  
    <li><a href="#">Link 13</a></li>  
    <li><a href="#">Link 14</a></li>  
    <li><a href="#">Link 15</a></li>   

    <!-- Target element @see data-target above,
    can contain markup alreadu ie. <a href="#" class="dropdown-toggle" data-toggle="dropdown">More <span class="caret"></span></a> & <ul class="dropdown-menu"></ul>
    if required elements are missing they will be created -->
    <li id="more-menu-1"></li> 
  </ul>
</nav>

You probably noticed the .sticky class on 2 of the li elements, well this allows you to make certain list items remaining visible all the time and prevent them going into the collapse dropdown menu. We've used this on our "brand" logo as we don't want that being included in the collapse nav as the screen width changes.

We've also implemented some useful options which you can check out below.


 Javascript

We'll dump the whole Javascript and then break it down and explain it for those of you who want to look deeper:

// Global variables
var collapseNavSelector = $('[data-toggle="collapse-nav"]'),
  collapseNavStickyClass = 'sticky';

// Custom function
// Read collapseNav data- & find elements, return data in neat object
// =========================
function collapseNavGetData(target) {
  // collapseNavTarget
  var collapseNavTarget = target.data('target') || null;
  collapseNavTarget = $(collapseNavTarget);
  
  // Check target exists
  if (collapseNavTarget.size() === 0) {
    return false;
  }
  collapseNavTarget.addClass('collapse-nav-target').addClass('dropdown');
  if (target.find(target.data('target')).size() > 0) {
    collapseNavTarget.addClass('sticky');
  }
  
  // collapseNavItems 
  var collapseNavItems = 'li';
  var collapseNavItemsNoSticky = target.find('> ' + collapseNavItems).not('.' + collapseNavStickyClass);
  collapseNavItems = target.find('> ' + collapseNavItems);
  
  // collapseNavParent
  var collapseNavParent = target.data('parent') || '.navbar';
  collapseNavParent = target.parents(collapseNavParent);
  
  // collapseNavWidthOffset & parent
  // Can be value or selectors of elements
  var collapseNavWidthOffset = target.data('width-offset');
  
  var data = {
    collapseNav:              target, // the data-toggle="collapse-nav" element
    collapseNavParent:        collapseNavParent,
    collapseNavTarget:        collapseNavTarget, // object of more menu target where items are moved to
    collapseNavTargetMenu:    collapseNavTarget.find('.dropdown-menu'),
    collapseNavItems:         collapseNavItems, // object of items within collapseNav to collapse, override with data-items="li"
    collapseNavItemsNoSticky: collapseNavItemsNoSticky, // object of items within collapseNav to collapse, override with data-items="li"
    collapseNavItemsSticky:   target.find('> ' + '.' + collapseNavStickyClass), // object of  sticky items within collapseNav
    collapseNavCollapseWidth: target.data('collapse-width') || 300, // a pixel width where the collapseNav should be fully collapse ie. on mobile
    collapseNavWidthOffset:   collapseNavWidthOffset || 0, // offset for width calculation, can be value or selectors of elements
    collapseNavWidth:         0 // collapseNav width based on space available
  };

  return data;
}

// Custom function
// Calculates collapseNav element width
// =========================
function collapseNavGetWidth(data) {
  var collapseNavParentWidth = data.collapseNavParent.width(),
    collapseNavWidth = 0, // fallback, will trigger collapse
    collapseNavParentMargins = {
      'left': parseInt(data.collapseNavParent.css('margin-left')),
      'right': parseInt(data.collapseNavParent.css('margin-right'))
    },
    collapseNavOutterSpace = {
      'margin-left': parseInt(data.collapseNav.css('margin-left')),
      'margin-right': parseInt(data.collapseNav.css('margin-right')),
      'padding-left': parseInt(data.collapseNav.css('padding-left')),
      'padding-right': parseInt(data.collapseNav.css('padding-right'))      
    };

  // Check for negative margins on parent
  if (collapseNavParentMargins.left < 0 || collapseNavParentMargins.right < 0) {
    collapseNavParentWidth = data.collapseNavParent.outerWidth(true);
  }
  
  // Check for padding & margins on trigger
  $.each(collapseNavOutterSpace, function(a, v) {
    collapseNavParentWidth -= v;
  });
    
  // Otherwise calculate width base on elements within
  if (collapseNavParentWidth > 0) {
    collapseNavWidth =  collapseNavParentWidth;
    
    // Process width offset
    if (data.collapseNavParent.find(data.collapseNavWidthOffset).size() > 0) {
      // Offset with element width(s) ie. other navbar elements with parent
      data.collapseNavParent.find(data.collapseNavWidthOffset).each(function() {
        collapseNavWidth -= $(this).outerWidth(true);
      });
    }
    else {
      // Offset with value
      collapseNavWidth -= data.collapseNavWidthOffset;
    }

    // minus sticky items
    data.collapseNavItemsSticky.each(function() {
      collapseNavWidth -= $(this).outerWidth(true);
    });
    
    if (collapseNavWidth <= 0 || collapseNavWidth <= data.collapseNavCollapseWidth) {
      collapseNavWidth = 0;
    }
  }
  return collapseNavWidth;
}

// Custom function that resizes menu
// =========================
function collapseNavResize(data) {
  var collapseItemsWidth = 0;
  
  // See how many "items" fit inside collapseNavWidth
  if (data.collapseNavWidth > 0 ) {
    data.collapseNavItemsNoSticky.each(function() {
      var collapseNavItem = $(this),
        collapseNavItemId = '.' + collapseNavItem.data('collapse-item-id');
      collapseItemsWidth += collapseNavItem.outerWidth(true);
        
      if (data.collapseNavWidth < collapseItemsWidth) {
        data.collapseNav.find(collapseNavItemId).addClass('collapse-item-hidden');
        data.collapseNavTargetMenu.find(collapseNavItemId).removeClass('collapse-item-hidden');
      }
      else {
        data.collapseNav.find(collapseNavItemId).removeClass('collapse-item-hidden');
        data.collapseNavTargetMenu.find(collapseNavItemId).addClass('collapse-item-hidden');
      }
    });
  }
  else {
    // Assume all collapsed
    data.collapseNavItemsNoSticky.addClass('collapse-item-hidden');
    data.collapseNavTargetMenu.find('.collapse-item').removeClass('collapse-item-hidden');
    data.collapseNav.width('auto');
  }
  
  // see if collapseNavTarget contains visible elements, :visible selector fails
  var visibleItems = data.collapseNavTargetMenu.find('.collapse-item').filter(function() {
    return $(this).css('display') !== 'none';
  }).size();

  if (visibleItems > 0) {
    data.collapseNavTarget.show();
  }
  else {
    data.collapseNavTarget.hide();
  }
  
}

// Run through all collapse-nav elements
// =========================
function collapseNavTrigger(setup) {
  collapseNavSelector.each(function() {
    var collapseNav = $(this),
      collapseNavData = collapseNavGetData(collapseNav);
      
    if (collapseNavData === false) {
      // No target so bail
      return false;
    }
  
    // Run setup only on first run
    // ---------------------------
    if (setup === true) {
      // Check target has <ul class="dropdown-menu"></ul> & data-toggle elements, if not create them
      if (collapseNavData.collapseNavTarget.find('[data-toggle="dropdown"]').size() === 0) {
        $('<a href="#" class="dropdown-toggle" data-toggle="dropdown">More <span class="caret"></span></a>').appendTo(collapseNavData.collapseNavTarget);
      }
      if (collapseNavData.collapseNavTarget.find('.dropdown-menu').size() === 0) {
        collapseNavData.collapseNavTargetMenu = $('<ul class="dropdown-menu"></ul>');
        collapseNavData.collapseNavTargetMenu.appendTo(collapseNavData.collapseNavTarget);
      }
      
      // clone $collapseNav > collapseNavItems into collapseNavTarget
      collapseNavData.collapseNavItems.each(function(i) {
        var collapseItem = $(this);
        collapseItem.addClass('collapse-item');
        
        if (!collapseItem.hasClass(collapseNavStickyClass)) {
          // Add identifier & class to each non-sticky item
          collapseItem.data('collapse-item-id', 'collapse-item-' + i).addClass('collapse-item-' + i);
          collapseItem.clone().appendTo(collapseNavData.collapseNavTargetMenu);
        }
      });
      
      collapseNavData.collapseNav.addClass('collapse-nav');
    }

    // Calulate navbar width
    // ---------------------------
    collapseNavData.collapseNavWidth = collapseNavGetWidth(collapseNavData);
    
    // Trigger menu resizing
    // ---------------------------
    collapseNavResize(collapseNavData);
  });
}

// Run on doc ready
// =========================
$(document).ready(function(){
  collapseNavTrigger(true);
  
  // On resize
  $(window).on('resize', function() {
    collapseNavTrigger(false);
  });
});
  • collapseNavGetData: like in previous freebies we use a function to read the data attributes from our trigger element and return them as an object for neatness.
  • collapseNavGetWidth: this function returns the width the trigger element has available to fill and that items larger than that width can be hidden. It calculates this by taking the width of the"parent" element (.navbar) minus any "offset" & .sticky items.
  • collapseNavResize: this function takes the available width calculated by collapseNavGetWidth and sees how many items within the trigger element "fit" inside that width. Items that don't fit are given the class .collapse-item-hidden which is set to display: none; in our CSS code below. Items are hidden with CSS to allow great flexibility and the option to add some transition effects. If the available is less than 0 all items are hidden.
    • collapseNavTrigger: this function triggers everything into action, it finds all data-toggle="collapse-nav" elements on the page, runs setup tasks on first run and then calls the above functions.
  • $(document).ready: this runs the collapseNavTrigger function when the page is loaded and also binds a resize event to refresh the collapse nav elements on resize.

CSS

Similar to the Javascript, we'll dump the core stuff here then explain them further for those who want to look deeper:

.collapse-nav:before,
.collapse-nav:after {
  content: " ";
  display: table;
}
.collapse-nav:after {
  clear: both;
}
.collapse-nav .navbar-brand,
.collapse-nav.navbarnav > li > a {
  padding: 15px;
}
.collapse-nav .collapse-item {
  float: left !important;
}
.collapse-item.collapse-item-hidden {
  /** .collapse-item-hidden is applied when item is hidden from view, could tweak this and add CSS transition effect **/
  display: none;
}
.collapse-nav-target .collapse-item {
  float: none !important;
}
.collapse-nav-target .dropdown-menu {
  position: absolute !important;
  background: white !important;
  -webkit-background-clip: padding-box !important;
  background-clip: padding-box !important;
  border: 1px solid #ccc !important;
  border: 1px solid rgba(0, 0, 0, 0.15) !important;
  border-radius: 0 !important;
  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175) !important;
  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175) !important;
  right: 0 !important;
  left: auto !important;
  margin-top: 0 !important;
}
.collapse-nav-target .dropdown-menu > li > a {
  color: #777 !important;
}
.collapse-nav-target .dropdown-menu > li > a:hover {
  background-color: #f5f5f5 !important;
}

Hopefully this is quite self explanatory but the most important bits are:

  • .collapse-nav .collapse-item {float: left !important;}: this forces the collapse items to stay inline and not collapse on mobile. If they were to collapse the whole effect wouldn't work.
  • .collapse-item.collapse-item-hidden {display: none;}: this hides items when the do not fit within our collapse nav trigger element. As mentioned, this is done with CSS rather than jQuery to allow more flexibility.
  • .collapse-nav-target .collapse-item {float: none !important;}: this ensures items within the collapse-nav-target dropdown element stack instead of float. You might want to adjust this to suite your site needs.

All other CSS is down to your site needs and can be altered. LESS files are included!!


Options

Like all our free Bootstrap downloads we added some useful options to help you customise this freebie to your needs.

data-parent="JQUERY-SELECTOR"

Default: .navbar

This refers to the parent element that wraps around the data-toggle="collapse-nav" element and is used to calculate the available width for the navbar. This is done because the data-toggle="collapse-nav" element varies constantly as items within it as hidden.


data-width-offset="JQUERY-SELECTOR or PIXEL-VALUE"

Default: 0

If jQuery selectors are passed then the width of the passed elements will be deducted from the parent width. Example: data-width-offset=".navbar-brand" If this is a pixel value the passed value will be deducted from the parent width. Example: data-width-offset="150"


data-collapse-width="PIXEL-VALUE"

Default: 300

If the parent elements available width is this amount or less then all items within the data-toggle="collapse-nav" element hide automatically hidden minus sticky elements.


Sum Up

Not as polished as the BBC version but the functionality is there to be built on.

Let us know what you think in the comments below.

License

This is free to use for personal use only, for commercial use please contact us.

icon icon