Make Internal Links Scroll Smoothly with JavaScript

Share this article

When they’re navigating through a long document, users often are confused or disoriented when they click a link that jumps to another location in that same document.

Are they on the same page, or a different page? Should they scroll more from here? What’s going on?

The answer to this problem is to scroll the user through the document to the linked location, like this. In this tutorial, we’ll use a smattering of JavaScript to ensure that links that are internal to the document scroll the user to their destination, rather than jumping straight there and confusing users.

Finding Internal Links

First, we need to identify all the links in the document, and then work out which of them are internal ones. Getting a list of all the links is easy:

  var allLinks = document.getElementsByTagName('a');

We need to walk through this list, and work out which of the links we’ve found are internal. An internal link will have a hash (#) symbol in it, and it will point to the document we’re currently looking at. The useful location object tells us about the URL of the document we’re looking at now, so try this:

  for (var i=0;i<allLinks.length;i++) { 
 var lnk = allLinks[i];
   if ((lnk.href && lnk.href.indexOf('#') != -1) &&  
       ( (lnk.pathname == location.pathname) ||
   ('/'+lnk.pathname == location.pathname) ) &&  
       (lnk.search == location.search)) {
          DO SOMETHING WITH THE LINK HERE
   }
 }

Here, the for loop walks through the list of links in the document, and we check for three things:

  1. Does the link contain a hash?
    We check this using the link’s href property, and the indexOf() function to find the location of one string in another.

  2. Is the link the same as the current location?
    Links (and the location object) have a pathname attribute. The pathname of the URL https://www.sitepoint.com/about/who/mharbottle.php is /about/who/mharbottle.php in some browsers, and about/who/mharbottle.php in others (note the presence or absence of the first slash). We must check for both.

  3. Is the querystring the same as the current location?
    The querystring is everything that appears after the ? in a url; this is obviously important if your site is database driven. JavaScript defines a search attribute on location and links that contain the querystring.

If each of these questions is true, then we know the link is an internal one, and we can set it to scroll to its destination.

Scroll, Don’t Jump!

Now we’ve identified an internal link, we want to make it scroll when it’s clicked. To do this, we’ll need to attach an onclick event handler to the link. In days of old, when Web developers were bold, many thought (well, I did) that event handlers were set on a link within the HTML:

<a href="https://www.sitepoint.com/" onclick="myEventHandler()">

But this isn’t really the truth; instead, you should attach an event listener to the link object. The W3C specifies a standard method to do this, as does Internet Explorer; Scott Andrew has usefully provided a function to handle both:

function ss_addEvent(elm, evType, fn, useCapture) 
// addEvent and removeEvent
// cross-browser event handling for IE5+,  NS6 and Mozilla
// By Scott Andrew
{
 if (elm.addEventListener){
   elm.addEventListener(evType, fn, useCapture);
   return true;
 } else if (elm.attachEvent){
   var r = elm.attachEvent("on"+evType, fn);
   return r;
 }
}

So, in our loop over the links, we call this script to attach a smooth-scroll function to the internal link:

ss_addEvent(lnk,'click',smoothScroll);

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.

Making the Effect Happen

The easiest way to apply this effect to your pages is to drop the code into a file called smoothscroll.js and include that file in your page using this code:

<script src="smoothscroll.js" type="text/javascript"></script>

This approach follows the principles of unobtrusive DHTML, making it easy for everyone to use. For the solution to work, the script needs to be run by something; we put the code from our first step (looping over the links to find those that are internal) into a function ss_fixAllLinks(), and bind that to the window’s onload event using Scott Andrew’s function:

ss_addEvent(window,"load",ss_fixAllLinks);

The whole code looks like this:

function ss_fixAllLinks() {   
 // Get a list of all links in the page  
 var allLinks = document.getElementsByTagName('a');  
 // Walk through the list  
 for (var i=0;i<allLinks.length;i++) {  
   var lnk = allLinks[i];  
   if ((lnk.href && lnk.href.indexOf('#') != -1) &&    
       ( (lnk.pathname == location.pathname) ||  
   ('/'+lnk.pathname == location.pathname) ) &&    
       (lnk.search == location.search)) {  
     // If the link is internal to the page (begins in #)  
     // then attach the smoothScroll function as an onclick  
     // event handler  
     ss_addEvent(lnk,'click',smoothScroll);  
   }  
 }  
}  
 
function smoothScroll(e) {  
 // This is an event handler; get the clicked on element,  
 // in a cross-browser fashion  
 if (window.event) {  
   target = window.event.srcElement;  
 } else if (e) {  
   target = e.target;  
 } else return;  
   
 // Make sure that the target is an element, not a text node  
 // within an element  
 if (target.nodeType == 3) {  
   target = target.parentNode;  
 }  
   
 // Paranoia; check this is an A tag  
 if (target.nodeName.toLowerCase() != 'a') return;  
   
 // Find the <a name> tag corresponding to this href  
 // 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;  
   
 // Find the destination's position  
 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;  
 }  
   
 // Stop any current scrolling  
 clearInterval(ss_INTERVAL);  
   
 cypos = ss_getCurrentYPos();  
   
 ss_stepsize = parseInt((desty-cypos)/ss_STEPS);  
 ss_INTERVAL = setInterval('ss_scrollWindow('+ss_stepsize+','+desty+',"'+anchor+'")',10);  
   
 // And stop the actual click happening  
 if (window.event) {  
   window.event.cancelBubble = true;  
   window.event.returnValue = false;  
 }  
 if (e && e.preventDefault && e.stopPropagation) {  
   e.preventDefault();  
   e.stopPropagation();  
 }  
}  
 
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;  
 }  
}  
 
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;  
}  
 
function ss_addEvent(elm, evType, fn, useCapture)  
// addEvent and removeEvent  
// cross-browser event handling for IE5+,  NS6 and Mozilla  
// By Scott Andrew  
{  
 if (elm.addEventListener){  
   elm.addEventListener(evType, fn, useCapture);  
   return true;  
 } else if (elm.attachEvent){  
   var r = elm.attachEvent("on"+evType, fn);  
   return r;  
 }  
}    
 
var ss_INTERVAL;  
var ss_STEPS = 25;  
 
ss_addEvent(window,"load",ss_fixAllLinks);
Wrapping Up

Your document internal links will scroll to their destination, allowing your users to retain an awareness of where the browser is located within the document, and how far they are from their starting point. The code has been tested and works in Mozilla, IE, and Opera; it doesn’t work in Konqueror, and is assumed to not work in other browsers.

Frequently Asked Questions (FAQs) about Smooth Scrolling with JavaScript

How can I implement smooth scrolling in JavaScript?

Implementing smooth scrolling in JavaScript involves using the window.scrollTo method. This method takes two arguments: the x-coordinate and the y-coordinate to which the window should scroll. To make the scrolling smooth, you can use the behavior property and set it to ‘smooth’. Here’s a simple example:

window.scrollTo({
top: 0,
behavior: 'smooth'
});
This code will smoothly scroll the window to the top of the page.

Can I use JavaScript to scroll to a specific element on the page?

Yes, you can use JavaScript to scroll to a specific element on the page. You can do this by first selecting the element using a method like document.querySelector, and then using the scrollIntoView method on the selected element. Here’s an example:

var element = document.querySelector('#myElement');
element.scrollIntoView({behavior: 'smooth'});
This code will smoothly scroll the window to the element with the id ‘myElement’.

What browsers support smooth scrolling with JavaScript?

The smooth scrolling feature in JavaScript is supported by most modern browsers, including Chrome, Firefox, Safari, and Edge. However, it’s not supported by Internet Explorer. You can check the compatibility table on the MDN Web Docs for the most up-to-date information.

Can I control the speed of the smooth scrolling?

The speed of the smooth scrolling is determined by the browser and cannot be directly controlled with JavaScript. However, you can create a custom smooth scrolling function with a set speed by using the window.requestAnimationFrame method.

How can I implement smooth scrolling with JavaScript for anchor links?

You can implement smooth scrolling for anchor links by adding an event listener for the ‘click’ event on the links. In the event handler, you can prevent the default action of the link, which is to instantly navigate to the target element, and instead use the scrollIntoView method to smoothly scroll to the target element. Here’s an example:

document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();

document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});
This code will add smooth scrolling to all anchor links on the page.

Can I add a scroll offset when using smooth scrolling with JavaScript?

Yes, you can add a scroll offset when using smooth scrolling with JavaScript. You can do this by subtracting the desired offset from the y-coordinate in the scrollTo method or the top property in the scrollIntoView method.

Can I use jQuery to implement smooth scrolling?

Yes, you can use jQuery to implement smooth scrolling. jQuery provides the animate method, which you can use to animate the scrollTop property of the html and body elements. Here’s an example:

$('html, body').animate({
scrollTop: $("#myElement").offset().top
}, 2000);
This code will smoothly scroll the window to the element with the id ‘myElement’ over a period of 2 seconds.

Can I implement smooth scrolling with CSS instead of JavaScript?

Yes, you can implement smooth scrolling with CSS by using the scroll-behavior property. You can set this property to ‘smooth’ on the html or body element to enable smooth scrolling for the whole page. However, this method has less browser support than the JavaScript method.

How can I test if smooth scrolling is working correctly?

You can test if smooth scrolling is working correctly by simply trying to scroll on your page. If the scrolling is smooth and not instant, then it’s working correctly. You can also use the developer tools in your browser to inspect the scroll behavior.

Can I disable smooth scrolling with JavaScript?

Yes, you can disable smooth scrolling with JavaScript by simply not using the behavior property in the scrollTo or scrollIntoView methods, or by setting it to ‘auto’. This will make the scrolling instant instead of smooth.

Stuart Langridge and Tony Steidler-DennisonStuart Langridge and Tony Steidler-Dennison
View Author

Stuart Langridge has been a Linux user since 1997, and is quite possibly the only person in the world to have a BSc in Computer Science and Philosophy. He’s also one-quarter of the team at LugRadio, the world's premiere Free and Open Source Software radio show. Tony Steidler-Dennison is a Systems Engineer with Rockwell Collins, Inc., designing avionics and cabin data servers for commercial airliners. He’s also the host of The Roadhouse Podcast, "the finest blues you've never heard."

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week