Animated Scrolling for Same-Page Links

Many months ago, I posted a note to the jQuery discussion list showing a script I wrote that uses the Interface plugin's ScrollTo() method to automatically scroll smoothly to any id or named anchor on the current page when clicking on a same-page link. The code was unwieldy and unneccessarily long, but it worked.

A little game of one-upmanship followed among some of the gurus, ultimately reducing my 18 lines of code to 11. And that made me happy—until I tried the code in Internet Explorer 6. It didn't throw an error, but the smooth scrolling didn't seem to work in that one sad-sack browser. Since I was writing the code for my day job and I didn't have a lot of extra time to investigate the issue, I just left my initial code in there and shrugged it off.

The other night, however, as I was digging around some old files, I came across the code from my discussion list friends again, so I decided to see if I could fix what ailed it in IE. This is what they had arrived at:

[js]$(document).ready(function() { $('a[href*="#"]').click(function() { if (location.pathname == this.pathname && location.host == this.host) { var target = $(this.hash); target = target.size() && target || $("[name=" + this.hash.slice(1) +']'); if (target.size()) { target.ScrollTo(400); return false; } }; }); });[/js]

This code does the following:

  1. attaches a click handler to all <a> elements that have the "#" symbol somewhere in their href attribute.
  2. in the click handler, checks to make sure that the current page's path name (location.pathname) is the same as the clicked link's path name (this.pathname) and the current page's host location (location.host) is the same as the clicked link's host (this.host). This ensures that the link is pointing to an item on the current page.
  3. stores the "hash" of the clicked link (i.e. the "#" and everything that follows it in the href) and wraps it in a jQuery object, which effectively makes it an <id> selector.
  4. uses some shorthand to keep the variable the same if there actually is an element with an id the same as the link's hash, or otherwise set it to a selector that uses the "name" attribute.

What's in a path name?

It turns out that IE6 includes the initial slash in location.pathname, but not in this.pathname. Firefox and Safari include the initial slash in both. For example, if we're on the page http://test.learningjquery.com/smooth-scrolling.htm and we click on a link with href="#foo", IE6 reads location.pathname as "/smooth-scrolling.htm" but this.pathname as "smooth-scrolling.htm"

To fix this problem, I just made the pathname properties consistent, adding a .replace() method to strip the initial slash if it's there:

if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') 
  && location.host == this.host)

But that wasn't quite enough. Something was still keeping it from working in IE6. Can you guess what else might break in that browser?

A port for this host

If you guessed this.host, you were right. Using the same example page, (http://test.learningjquery.com/smooth-scrolling.htm) and the same href ("#foo"), IE6 reads location.host as "test.learningjquery.com" but this.host as "test.learningjuery.com:80". So, the two don't match, and the ScrollTo() won't run. The solution? Use hostname instead.

Here is the finished code:

[js]$(document).ready(function() { $('a[href*=#]').click(function() { if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) { var $target = $(this.hash); $target = $target.length && $target || $('[name=' + this.hash.slice(1) +']'); if ($target.length) { $target.ScrollTo(400); return false; } }; }); });[/js]

If you look closely, you'll notice a few other minor differences between these two code blocks. They're just a matter of personal preference, but I might as well point them out:

  1. changed $('a[href*="#"]') to $('a[href*=#]') (removed the double quotes around the pound symbol) because the quotes aren't necessary.
  2. changed var target = $(this.hash) to var $target = $(this.hash). This is the tiniest of changes, but like others do, I always begin variables with "$" when they refer to a jQuery object. It just makes it easier for me to return to my code and understand what those variables stand for.
  3. changed .size() to .length. I don't think there is any real difference between these two, but my purely personal preference is .length.

Try it out

If you'd like to see this animated scrolling in action, click here. Or, from the home page, click on one of the links in the drop-down page contents at the top-right corner of the page.

If you want to implement this on your own site, you should put the above code in a .js file and refer to it in the <head> of your document. You'll also need jquery.js, of course, and some form of the Interface plugin suite. I packaged up only the components I needed for ScrollTo() -- iutil.js, ifx.js and scrollto.js. You can download my Interface bundle here.

What about ScrollToAnchors() ?

You might be wondering why I'd go through all this trouble if Interface already has a ScrollToAnchors() method that ostensibly does the same thing. Well, it doesn't make sure that location.pathnme and this.pathname are the same before running the code and returning false. So, if we had a link to an anchor on some other page, the ScrollToAnchors() method wouldn't let us go there. And that would be a shame. Besides, if I recall correctly, this method was added after I started work on my homespun solution.

Update

I've posted a new entry about how to achieve the same effect (and more) using jQuery 1.2, without the need for any of the Interface plugin modules: Animated Scrolling with jQuery 1.2. [Posted an improved version Oct. 20, 2007]