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:
- Listen (
bind
) for when the user scrolls the window - Find the topmost
h1
element that matches the:in-viewport
selector - Use that
h1
element to find theid
from the container, and use that to find the related navigation link - If the navigation link isn’t already selected, the remove the selected class from all the navigation links, and finally -
- 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 h1
s 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:
- Did we actually match anything, using
length
- 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');
}
});
You should follow me on Twitter here I tweet about jQuery amongst the usual tweet-splurges!