Article
Make Internal Links Scroll Smoothly with JavaScript
How to Scroll
Of course, we have to actually have a smoothScroll() function, too. This is the complicated aspect, because it's all about finding an object's position on the page, and different browsers implement this in various ways. The marvelous Andrew Clover has written a summary of how to find this position across browsers and we’ll use this solution extensively here.
First, our smoothScroll function is an event handler, so, when it’s called (i.e. when a user clicks one of our internal links) we need to retrieve the link that was clicked. Netscape-class browsers pass an event object to each handler; Internet Explorer stores these details in the global window.event object.
if (window.event) {
target = window.event.srcElement;
} else if (e) {
target = e.target;
} else return;
This code sets the clicked link as the target in a cross-browser fashion. …well, nearly. Mozilla will sometimes pass you the text node within a link as the clicked-on item. We need to check whether target is a text node (i.e. whether its nodeType is 3), and take its parent if it is.
if (target.nodeType == 3) { target = target.parentNode; }
Just to be paranoid, we also check that what we've got is an A tag, in case we've missed something:
if (target.nodeName.toLowerCase() != 'a') return;
Now, we need to find the destination: the <a name> tag that corresponds to the part after the hash in our clicked-on link. Links have a hash attribute that contains the # and the section that appears after it in the URL, so let’s now walk through all the links in the document and check whether their name attribute is equal to the hash part of the clicked-on link:
// First strip off the hash (first character)
anchor = target.hash.substr(1);
// Now loop all A tags until we find one with that name
var allLinks = document.getElementsByTagName('a');
var destinationLink = null;
for (var i=0;i<allLinks.length;i++) {
var lnk = allLinks[i];
if (lnk.name && (lnk.name == anchor)) {
destinationLink = lnk;
break;
}
}
// If we didn't find a destination, give up and let the browser do
// its thing
if (!destinationLink) return true;
We know what we clicked on, and what that points to. Now all we need to know is where we are in the document, and what our destination is. This is where Andy Clover's notes are invaluable. First, we find the position of the destination link:
var destx = destinationLink.offsetLeft;
var desty = destinationLink.offsetTop;
var thisNode = destinationLink;
while (thisNode.offsetParent &&
(thisNode.offsetParent != document.body)) {
thisNode = thisNode.offsetParent;
destx += thisNode.offsetLeft;
desty += thisNode.offsetTop;
}
Note that we loop through offsetParents until we get to the document body, as IE requires. Next, work out where we are currently located:
function ss_getCurrentYPos() {
if (document.body && document.body.scrollTop)
return document.body.scrollTop;
if (document.documentElement && document.documentElement.scrollTop)
return document.documentElement.scrollTop;
if (window.pageYOffset)
return window.pageYOffset;
return 0;
}
IE5 and 5.5 store the current position in document.body.scrollTop, IE6 in document.documentElement.scrollTop, and Netscape-class browsers in window.pageYOffset. Phew!
The way we actually handle the scrolling is to use setInterval(); this thoroughly useful function sets up a repeating timer that fires a function of our choice. In this case, we'll have our function move the browser's position one step closer to the destination; setInterval() will call our function repeatedly, and when we reach the destination, we'll cancel the timer.
First, use clearInterval() to turn off any timers that are currently running:
clearInterval(ss_INTERVAL);
ss_INTERVAL is a global variable in which we will later store the ouput of setInterval(). Next, work out how big each step should be:
ss_stepsize = parseInt((desty-cypos)/ss_STEPS);
ss_STEPS is defined in the script to be the number of steps we take from target to destination. Our "scroll one step" function is called ss_scrollWindow and takes three parameters:
- how much to scroll
- the destination position
- the destination link itself
We need to construct a call to this in a string, and pass that string to setInterval, along with the frequency with which we want the call repeated:
ss_INTERVAL = setInterval('ss_scrollWindow('+ss_stepsize+','+desty+',"'+anchor+'")',10);
Notice how we're building up a string that's a call to ss_scrollWindow(), rather than just calling ss_scrollWindow() directly -- this is one of the most confusing things about setInterval().
Once we've done that, we have to stop the browser taking its normal course by obeying the link and jumping directly to the destination. Again, this happens differently in different browsers. To stop the browser handling this event normally in Internet Explorer, use:
if (window.event) {
window.event.cancelBubble = true;
window.event.returnValue = false;
}
Notice the check for window.event to ensure that we're using IE.
To do the same in Netscape-class browsers, use this code:
if (e && e.preventDefault && e.stopPropagation) {
e.preventDefault();
e.stopPropagation();
}
Scrolling a Step
One last thing: how do we actually do the scrolling? The key function here is window.scrollTo(), to which you pass an X and Y position; the browser then scrolls the window to that position. One minor wrinkle is that you can't scroll all the way to the bottom. If the Y position you pass in is less than a window's height from the bottom of the document, the browser will scroll down only as far as it can -– obviously it can’t go right down to the link if the distance to the bottom of the page is less than the height of the window.
Now, we need to check for that; the best way to do so is to see whether the positions before and after the scroll are the same:
function ss_scrollWindow(scramount,dest,anchor) {
wascypos = ss_getCurrentYPos();
isAbove = (wascypos < dest);
window.scrollTo(0,wascypos + scramount);
iscypos = ss_getCurrentYPos();
isAboveNow = (iscypos < dest);
if ((isAbove != isAboveNow) || (wascypos == iscypos)) {
// if we've just scrolled past the destination, or
// we haven't moved from the last scroll (i.e., we're at the
// bottom of the page) then scroll exactly to the link
window.scrollTo(0,dest);
// cancel the repeating timer
clearInterval(ss_INTERVAL);
// and jump to the link directly so the URL's right
location.hash = anchor;
}
}
Note that, because we scroll in specific integral increments, this step might have taken us past our destination. Thus, we check whether we were above the link before and after the scroll; if these two locations are different, we've scrolled past the link, and as such, we've finished. If we're finished, we cancel the timer and set the page's URL (by setting a bit of the location object) so that it looks as if the browser had handled the link.