Search

Scroll Linked Navigation

Posted on 18th August 2010 — There are a few websites I’ve seen lately that have a left hand navigation automatically updates it’s selection based on where you’ve scrolled to on the page. This tutorial will show you exactly how to achieve just that.

Watch

Watch Scroll Linked Navigation screencast (Alternative flash version)

QuickTime version is approximately 50Mb, flash version is streaming.

View the demo used in the screencast

The Problem

What you need to do is hook in to the user scrolling the web page, and when a particular element comes in to view, if there’s an associated navigation item – make it selected. That’s the trick, the hard part is working out exactly which element is now in view.

Fortuantly there’s a jQuery plugin for that particular job that makes it really easy to select only those elements that are in the viewport (as well as outside the viewport): viewport selectors plugin.

It’s not a new plugin by any means, but you need to have a specific reason to want to use it – and it fits this job perfectly.

Solving using the viewport selector plugin

Using the sample page I’ve mocked for this example, if you test a jQuery selector using your favourite debugging tool we can work out which section of the page the user is looking at. For this example, I’m going to say the navigation is linked to the topmost h1 in view (from within the console):

$('h1:in-viewport').length

This .length test should give me one or more. Using the h1 element, I’ll navigate up the DOM to find the parent element and use it’s id to update the navigation as appropriate.

Using the id from our container element, we’ll look for a navigation link that contains a hash that matches the id. This is a common pattern that I’ve used several times before, and will look something like this where ourID is the id we got using the viewport selector:

$('nav a[hash=#ourID]')

Don’t worry about that bit just yet, once you see it all working you should get a better idea of how it all fits together.

Before we crack on with the jQuery code required to make this all work, make sure you’ve included both jQuery then the jQuery viewport selectors plugin:

<script src="jquery-1.4.2.js"></script>
<script src="jquery.viewport.js"></script>

Now, we’re ready to rock.

The Solution

We need to satisfy the following steps to make this simple effect work:

  1. Listen (bind) for when the user scrolls the window
  2. Find the topmost h1 element that matches the :in-viewport selector
  3. Use that h1 element to find the id from the container, and use that to find the related navigation link
  4. If the navigation link isn’t already selected, the remove the selected class from all the navigation links, and finally -
  5. Add the selected class to the found navigation link

Using this breakdown, let’s start adding the code in. First listen for when the user scrolls:

$(window).scroll(function () {
  
});

We need to listen on the window because it’s the whole window the user can scroll. If you wanted to localise this technique to a specific element with overflow, you would use something like $('#foo').scroll(fn).

Our h1s are inside the #content > section selector, but I want to allow for more than h1 in the section, need to find all that are in the viewport, then narrow to the first:

$(window).scroll(function () {
  var inview = $('#content > section > h1:in-viewport:first');
});

Now the inview variable will have a jQuery object. This isn’t what we want, we want the id and we need it to match the hash attribute on a link. So if the id is ‘first’, the hash should be ‘#first’. So let’s get the id from the parent section element and get the id attribute and finally prepend a # symbol to create the hash:

$(window).scroll(function () {
  var inview = '#' + $('#content > section > h1:in-viewport:first').parent().attr('id');
});

Normally I wouldn’t use the attr('id') method to get the element id, I would do something like .parent()[0].id, but because we’re using the :in-viewport selector, there’s a good chance that the selector will fail to match anything if we scroll too far, so using jQuery’s attr is a safe way of getting some value without our code breaking.

Next we’ll use this hash to find the navigation link:

$(window).scroll(function () {
  var inview = '#' + $('#content > section > h1:in-viewport:first').parent().attr('id'),
      $link = $('nav a').filter('[hash=' + inview + ']');
});

Make sure you try out this newly added line of code in your debugger – swapping the variable inview for a string like ‘#first’, etc. It’ll show you which element it’s selecting.

There’s two tests we need to make before continuing:

  1. Did we actually match anything, using length
  2. Only proceed if the link isn’t already selected
$(window).scroll(function () {
  var inview = '#' + $('#content > section > h1:in-viewport:first').parent().attr('id'),
      $link = $('nav a').filter('[hash=' + inview + ']');
      
  if ($link.length && !$link.is('.selected')) {
    
  }
});

Finally we remove the selected class from the existing navigation and add it to the correctly found $link navigation:

$(window).scroll(function () {
  var inview = '#' + $('#content > section > h1:in-viewport:first').parent().attr('id'),
      $link = $('nav a').filter('[hash=' + inview + ']');
      
  if ($link.length && !$link.is('.selected')) {
    $('nav a').removeClass('selected');
    $link.addClass('selected');    
  }
});