More Than Just Web Design | INTERNET ENGINEERING | APPLICATION | DESIGN

Bootstrap Affix Plugin

Posted: 31/03/16

The Bootstrap Affix plugin is pretty cool, but using it properly isn't particularly obvious.

We've been using Bootstrap 3 for a number of projects recently, and one plugin that we make use of is the Affix plugin. This plugin is especially handy on mobile devices, because it can be used to nail the nav menu to the top of the page and have it stay there when the page is scrolled. Since mobile pages tend to be a lot longer, having the nav menu "always on" makes a lot of sense. Unfortunately, the Bootstrap documentation doesn't quite articulate exactly what you need to do.

Because the screen size can change on different devices and if your menu is not at the top of the page, it's essential to calculate the position at which the menu becomes fixed. We did this by using jQuery to calculate the height of the element(s) preceding the menu. Another point to note is that it's better to do this when the page has loaded, rather than when it's "ready" because other elements such as slide shows may still be starting up and will cause the height to change. Thus rather than using the data- attributes in the HTML markup, we load the plugin with JavaScript:

$(window).load(function(){
    $('#headerNavWrapper').affix({
        offset: {
        top:  function(){ return $('#headerSlides').outerHeight(true)},
        bottom: 50}
    });
});
  • "headerNavWrapper" is the id of the element containing the menu
  • "headerSlides" is the element that sits above the menu whose height we need to calculate
  • We use a function rather than the more obvious $('#headerSlides').outerHeight(true) because Chrome doesn't seem to handle it and thinks the height is 0 and thus affixes with the first movement of the mouse. Stupid Chrome.

This works well, but it looses the plot when the screen is resized, which on a mobile may well happen if the user decides to turn the phone to landscape or portrait. The trick here is to listen for the window resize event and then reset the plugin.

Oh wait, there isn't actually a method to reset! No problem; because the plugin stores data against the element, the trick is to manipulate the data thus:

$(window).resize(function() {
    $('#headerNavWrapper').data('bs.affix').options.offset.top = $('#headerSlides').outerHeight(true);
});

The final point to note is that the styling of the fixed element is your responsibility. Bootstrap specifies "position: fixed" in the .affix class it assigns to the element, but it may not be fixed in the place you expect. Typically if there's an element above the menu, you will be left with a gap above the fixed menu which looks very odd.

Therefore a little CSS is in order:

.affix {
    z-index: 250;
    width:100%;
    top: 0;
}

We force the z-index to 250 so it sits on top of any element on the page. You don't want it running for cover behind a slide show. The width is forced to 100%, since the original nav was within a "container" div which has a max width of 1200px. Finally, top:0 forces the menu to sit right at the top of the page and stay there, which is what we were after.

But wait, there's more. Because the nav bar is now fixed, it no longer occupies space in the page, so the page effectively shrinks in length by the height of the nav bar. This can be a problem for two reasons:

  • The content directly below the nav bar is now covered by the nav bar
  • On pages of a particular length, these few pixels can be the difference between a browser scroll bar appearing and not. This has the unfortunate effect of the plugin thinking it needs to affix/unaffix in rapid succession leading to a bouncing display that shouldn't be viewed by an epileptic.

There is a fix for this, and it's to listen for some of the custom events that the plugin fires. In particular "affix.bs.affix" and "affix-top.bs.affix".

These two are fired just before the element is about to be affixed, and unaffixed (is that actually a word?). What we need to do is inject a margin into the top of the element below the nav bar, moving it down the page, leaving space for the navbar and thus keeping the page length intact. This is done like so:

$('#headerNavWrapper').on('affix.bs.affix', function(){
    $('#Main').css('margin-top',$('#headerNavWrapper').outerHeight(true)+'px');
}).on('affix-top.bs.affix', function(){
    $('#Main').css('margin-top','0');
});

Main is the id of the element below the nav bar. We calculate the height of the nav bar and add this many px to the top margin of Main. Of course all this jQuery can be chained together so the final code is:

$(window).load(function(){
    $('#headerNavWrapper').affix({
        offset: {
            top:  function(){ return $('#headerSlides').outerHeight(true)},
            bottom: 50}
    }).on('affix.bs.affix', function(){
        $('#Main').css('margin-top', $('#headerNavWrapper').outerHeight(true)+'px');
    }).on('affix-top.bs.affix', function(){
        $('#Main').css('margin-top','0');
    });
}).resize(function() {
    $('#headerNavWrapper').data('bs.affix').options.offset.top = $('#headerSlides').outerHeight(true);
});

...and if you're feeling a bit saucy, you can add other animations or CSS effects onto the nav bar, for example, if you include the awesome animate.css:

.on('affix.bs.affix', function(){
    $('nav','#headerNavWrapper').addClass('animated rubberBand');
});

Your nav bar will bounce like a rubber band when it's affixed.