Advertisement

Create a Sticky Navigation Header Using jQuery Waypoints

by

In this tutorial, we'll be creating a navigation bar that stays with you as you scroll down — and we'll also throw a gimmick or two into the mix to polish it off.

Republished Tutorial

Every few weeks, we revisit some of our reader's favorite posts from throughout the history of the site. This tutorial was first published in March of 2012.


Introduction

“Everybody loves ribbons” Chris Coyier says when discussing the virtues of the :before and :after pseudo-elements. I’ve seen these stylized, triangle-edged ribbons popping up all over the Internet (one prominent example being Facebook’s Introducing Timeline page) and, while they have a certain appeal, I have to admit that the spatial effect that they create just doesn’t look right to me.

Ribbons are liked for a reason, though — they break the mostly flat design paradigm that we’re traditionally bound to, and they’re one of the few visual elements to do so in an inconspicuous way. But, as the age-old saying goes, there ought to be more than one way to skin a cat — so, in this tutorial, I will propose an alternative visual style for such elements, which I find much more natural-looking and aesthetically pleasing. I hope you like it and make good use of it!

What We’ll be Doing

In this tutorial, we're going to use one of HTML5's new elements, the nav tag, as a container for a horizontal list of links. I’ll briefly explain how to make it look pretty using a little bit of CSS.

Most importantly, you’ll make yourself familiar with the basics of jQuery’s Waypoints plugin, which will provide advanced functionality: as the user scrolls down, the navigation bar will stick to the top of the viewport, as well as change to indicate the current section. As a little added touch, we’ll use another plugin, ScrollTo, in order to provide smooth scrolling and convenient positioning when the user clicks on the navigation links.


Step 1: The Box

I'm sure you're already familiar with the various new elements that have been introduced with HTML5. In this example we're going to make use of two of them: <nav> and <section>. We’re going to start out with the following:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div class=wrapper>
      <h1>A reasonably large heading.</h1>
      <nav><!-- Navigation. --></nav>
      <section id="section-1">Lorem ipsum ...</section>
      <!-- Type in some longer sections of sample text here. -->
    </div>
    <!-- Scripts will go here. -->
  </body>
</html>

We’re going to have to give our navigation bar an explicit width. Make it 28px wider than the wrapper, and nudge it into place with a negative left margin. Let’s also give it gently rounded top edges using border-*-radius, as well as some arbitrary padding.

nav {
  position: relative;
  width: 928px;
  padding: 2em;
  margin-left: -14px;
  border-top-left-radius: 14px 7px;
  border-top-right-radius: 14px 7px;
}

Next, we're going to add an unordered list of links inside the navigation bar, and set its items to display: inline-block in order to put them all on a single line. We don't need any bullets, so we'll also throw list-style: none into the mix.

<nav>
  <ul>
    <li><a class=nav-link href=#section-1>Section 1</a></li>
    <li><a class=nav-link href=#section-2>Section 2</a></li>
    ...some more list items...
  </ul>
</nav>
nav li {
  display: inline-block;
  list-style: none;
}

Up until now, you should have something like this:


Step 2: The Edges

Now, if only CSS allowed us to have multiple pseudo-elements (i.e. ::after::after), we could easily complete the rounded edges of the navigation bar in a semantically clean way. But we can’t do that, so we’ll need to add two non-semantic divs at the end of the nav. Give them the classes nav-left and nav-right (or you could call them something imaginative, like Castor and Pollux). They’re 14px wide by 14px tall, and are absolutely positioned 14px from the bottom end of the nav.

As seen above, the border-radius family of properties can take two values for each corner. Those can also be percentages of the element’s width, which is quite handy — this approach allows the border radius to automatically adapt to changes in the box’s dimensions.

The small "shadows" that complete the navigation bar’s ribbon look are created using ::after pseudo-elements. Their width and height, as well as their border radii, are also set using percentages.

/* swap ‘left’ with ‘right’ for the other two */

.nav-left {
  position: absolute;
  left: 0;  bottom: -14px;
  width: 14px;  height: 14px;
  background: #848a6a;
  border-bottom-left-radius: 100% 50%;
}

.nav-left::after {
  content: '';
  position: absolute;
  right: 0;
  width: 66%;  height: 66%;
  background: #000;
  border-top-left-radius: 100% 50%;
  border-bottom-left-radius: 100% 50%;
}

And we're done here!

Having gotten rid of most of the impossible geometry of straight-edged ribbons, let's carry on.


Step 3: The Script

To achieve the floating header effect, we’ll use a jQuery plugin called Waypoints, by Caleb Troughton. Its only purpose is to trigger events when the user scrolls to a certain element. As you will see, it is extremely simple, yet offers a lot of flexibility — you can have a look at several examples on its homepage.

Include jQuery and Waypoints into your page, and let’s get started!

The first think you’ll need to do is register a waypoint by calling the .waypoint() method on an element. Of course, this does nothing by itself — you’ll have to define a handler function to the event. The easiest way to do this is by passing that function as a parameter to .waypoint().

Try it now: add the following to your script and see a message pop up as you scroll past the navigation bar.

$(function() {                     // When the page has loaded,
  $('nav').waypoint(               // create a waypoint
    function() {
      alert("Waypoint reached.");
    }
  )
});

Now, in order to achieve our desired effect, things will necessarily get a bit more complicated. First, we need to enclose our navigation bar in a container, which will be our actual waypoint, and serve as a convenient placeholder (more on this below).

<div class="nav-container">
  <nav>
    ...
  </nav>
</div>

In your CSS, create the following CSS rule. (While you’re at it, move any vertical margins the nav might have to the nav-container)

.sticky {
  position: fixed;
  top: 0;
}

And we’re ready for the good part! Change your script’s contents to the following:

$(function() {

  // Do our DOM lookups beforehand
  var nav_container = $(".nav-container");
  var nav = $("nav");

  nav_container.waypoint({
    handler: function(event, direction) {
      	nav.toggleClass('sticky', direction=='down');
    }
  });

});

Okay, where the heck did all of this come from, you rightfully ask. Well, you’ve probably figured out that we’re attaching a waypoint to the nav-container; only this time, we’re doing it differently. Instead of directly passing the handler function to .waypoint(), we’re encapsulating it in an object. On its own, this makes no difference: both are completely valid ways of doing the same thing. The object that we’re passing, though, can contain several other option values — so using it now makes for more consistent code later on.

The handler function that we’ve defined receives two parameters: the first one is the standard jQuery event object, which is of little interest here. The second one is Waypoints-specific: it is a string whose value is either ‘down’ or ‘up’ depending on which way the user was scrolling when they reached the waypoint.

Now, knowing which way the user is going is a very important bit of information to have, simply because it allows us to execute different behavior in either direction. In the body of the handler function, we’re using a comparatively little known variant of jQuery’s .toggleClass() method, which makes for a useful shorthand: in this syntax, the second parameter determines whether the class will be added to the target element or removed from it. When the user scrolls down, the expression direction===’down’ evaluates to true, so our navigation bar receives the sticky class, and sticks to the top of the viewport. As soon as the user scrolls back up again, the class is removed from the navigation bar, which returns to its place. Try it out now.

Cool, huh? However, if you slowly scroll down past the waypoint which you’ve just created, you’ll probably notice that as you pass it the content “jumps” up a little due to the navigation bar getting removed from the content flow. Besides looking very sloppy, such behavior can possibly obscure part of your content and impair usability. Thankfully, all it takes is a simple fix — adding the following code to your handler function makes the jump go away.

if (direction == 'down')
  nav_container.css({ 'height':nav.outerHeight() });
else
  nav_container.css({ 'height':'auto' });

What’s going on here is fairly obvious: we use nav-container as a placeholder, as mentioned above. When we’re scrolling down, we expand its height, and the content below stays in place. There’s a catch, though - for this to work as it is, any vertical margins that you might want to have around the navigation bar should be applied to nav-container and not to the nav.

So there it is! We’ve got ourselves a nice fixed navigation bar, just like many other sites already do. Show’s over, folks…

…or is it? Well, there’s still a trick or two you might want to see, and which might just put you ahead of the pack. If that’s the case, read on.


Step 4: Vertical Offsets

If you think about it, there are many cases where triggering an event when an element reaches the very edge of the browser viewport isn’t what you want to do. Luckily, Waypoints provides a convenient option for that: offset. Here’s what it looks like:

nav.waypoint( {
  handler: …,
  offset: 50
} )

offset allows you to create the actual waypoint at a variable distance from the top of the element. A positive value triggers the waypoint when the element’s top is at the specified distance below the top of the viewport, and a negative value triggers the waypoint when the element is that far above the top of the viewport (i.e. the user has scrolled well past it).

The value of offset can be a number (representing a fixed amount of pixels), a string containing a percentage (interpreted as a percentage of the viewport’s height), or a function that returns a number of pixels. The last one can provide for some serious flexibility, and we’re going to make some use of it later on. For now, let’s stick to fixed values and see what they’re good for.

The first thing that comes to mind is adding some space above the sticky element. Using the offset variable, this is easy: for a 15-pixel offset from the top, add offset:15px to .waypoint()’s options, and change top:0px to top:15px in the .sticky CSS rule.

If your design calls for it, a small gradient above the navigation bar could also be a nice touch. This is easily accomplished by adding yet another div inside the nav, and writing a little bit of CSS:

.sticky .nav-above {
  position: absolute;
  top:-15px;
  left:1em;
  right:1em;
  height:15px;
  background: linear-gradient(top, rgba(255,255,255,1) 0%,rgba(255,255,255,0) 100%);
  /* add cross-browser gradient code as needed */
}

This pretty subtle bit of eye candy would work nicely in minimalistic designs.


Step 5: Offset Functions

One thing that Caleb has thoughtfully included in Waypoints is being able to generate a waypoint’s offset dynamically, like so:

nav.waypoint( {
  handler: …,
  offset: function() {
    return —(nav.outerHeight()+50);
  }
} )

This allows us to have a handler which would trigger when the user has already scrolled 50px past the bottom of the element, without needing to know its height in advance.

Note: Such procedurally generated offsets (as well as ones given as percentages), are recalculated every time the window is resized, new waypoints are added, or a waypoint’s options are modified. If you’re doing something else which could affect the positions of the waypoints (such as changing the DOM or page layout), be sure to call $.waypoints('refresh') afterwards for the positions to be recalculated.

In the context of our tutorial, one use for this functionality is smoothly sliding the navigation bar from the top. Be prepared — the following is the largest chunk of code so far. There should be nothing in it that you aren’t already familiar with, though, so here goes:

var top_spacing = 15;
var waypoint_offset = 50;

nav_container.waypoint({

  handler: function(event, direction) {

    if (direction == 'down') {

      nav_container.css({ 'height' : nav.outerHeight() });
      nav.addClass("sticky")
         .stop()
         .css("top", -nav.outerHeight())
         .animate({"top" : top_spacing});

    } else {

      nav_container.css({ 'height' : 'auto' });
      nav.removeClass("sticky")
         .stop()
         .css("top", nav.outerHeight() + waypoint_offset)
         .animate({"top" : ""});

    }

  },

  offset: function() {
    return &mdash;(nav.outerHeight() + waypoint_offset);
  }

});

Not too shabby! It’s all pretty standard jQuery fare: as soon as we add or remove the sticky class to the nav, we override the element’s vertical position using .css(), and then .animate() it to what it should be. The .stop() serves to prevent possible bugs by clearing jQuery’s event queue.

There’s a little side effect to this, though — since the code effectively takes over the navigation element’s vertical position when fixed, you might as well drop the top:15px declaration from your CSS. If you’re part of a big project, with separate people working on design and front-end scripting, this might constitute a problem since it’s easy to lose track of such hacks. Just to let you know, there exist plugins — such as Ariel Flesler’s excellent jQuery.Rule which can be used to bridge the gap between scripts and stylesheets. You’ll have to decide for yourself whether you need something like that.

You could surely achieve a similar effect with CSS @keyframes instead, but there is much less support for them (and a lot of vendor prefixes), they’re less flexible, and the “up“ animation would be a big no-no. Since we’re not leaving the track of progressive enhancement, there’s no reason not to stick with jQuery’s robust functionality.


Step 6: Highlighting and Smooth Scrolling

You might find yourself needing to change which item is highlighted as the reader progresses past different sections of your page. With Waypoints, this is quite easy to achieve. You’ll need to add the following to your script:

$(function() {
  …
  // copy from here…

  var sections = $('section');
  var navigation_links = $('nav a');
 
  sections.waypoint({
    handler: function(event, direction) {
      // handler code
    },
    offset: '35%'
  });

  // …to here
});

This time we’re using an offset expressed as a percentage of the window’s height. Effectively, this means that the imaginary line that tells our script what section is currently being viewed is placed about a third from the top of the viewport — right about where the viewer would be looking at when reading a long text. A more robust solution could use a function to adapt to changes in the navigation bar’s height.

The code that we’re going to use in our handler function is a little less self-explanatory, though. Here it is:

      var active_section;
      active_section = $(this);
      if (direction === "up") active_section = active_section.prev();

      var active_link = $('nav a[href="#' + active_section.attr("id") + '"]');
      navigation_links.removeClass("selected");
      active_link.addClass("selected");

First, we need to know which section is currently being viewed. If we’re scrolling down, then the section that the waypoint belongs to is the same one that becomes active. Scrolling up past a waypoint, however, means that it is the previous section that is brought into view — so we use .prev() to select it. Then, we remove the selected class from all links in the navigation bar, before re-applying it to the one whose href attribute corresponds to the id of the currently active section.

This works pretty nicely; if you would like to go beyond adding and removing classes, you might want to look into a plugin such as LavaLamp.

At some point, you may have noticed that clicking on the links in the navigation bar places the top of the section at the very top of the browser viewport. This is counterintuitive enough when there’s nothing obscuring that part of the screen; now that we’ve got a navigation bar there, it becomes a huge annoyance. This is where Ariel Flesler’s ScrollTo comes to the rescue. Include it in your page, and then add the following code:

  navigation_links.click( function(event) {

    $.scrollTo(
      $(this).attr("href"),
      {
        duration: 200,
        offset: { 'left':0, 'top':-0.15*$(window).height() }
      }
    );

  });

(of course, that code goes above the last curly brace!)

Although there are better ways to bind a function to click events, we’re going to stick with the most simple one: .click(). The .scrollTo() method is called in a way that is very similar to .waypoint(). It takes two parameters — a scroll target and an object containing different options, which, in this case, are pretty self-explanatory. The clicked link’s href attribute works nicely as a scroll target, and the expression used as the top offset places the target at 15% of the viewport’s height.


Conclusion

And it seems that we’re done. I’ve introduced you to the handy little plugin that is Waypoints, and we’ve gone over some usage cases that should give you an idea of the various things you could accomplish with it. We’ve also implemented a much more intuitive scrolling behavior to go with our navigation. Throw some Ajax into the mix, and you’re on your way towards building the kind of seamless, immersive Web experience that is the going to be future of the Web… well, more likely it will be trendy for a short while and then become commonplace, making Web veterans nostalgic about the way things used to be. But hey, that’s how things go.

As for ribbons, their greatest drawback is this: they’re just an illusion. The sides of the ribbon don’t actually go around the edges of the container; they only appear that way, which becomes fairly obvious as the ribbon goes over an element which sticks out from the edge of the page.

Due to how z-index works, there seems to be no simple way of resolving this conflict, save for avoiding it in the first place. However, with some imagination, as well as a basic knowledge of jQuery, you could engineer a way for these elements to move out of the ribbon’s way as it approaches them. Doing this is well beyond the scope of this tutorial, though; hopefully, I’ll be able to show it to you sooner or later in the form of a quick tip, either here or on Nettuts+. Stay tuned!

Advertisement