Animated Scrolling for Same-Page Links

read 40 comments

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:

JavaScript:
  1. $(document).ready(function() {
  2.   $('a[href*="#"]').click(function() {
  3.      if (location.pathname == this.pathname && location.host == this.host) {
  4.        var target = $(this.hash);
  5.        target = target.size() && target || $("[name=" + this.hash.slice(1) +']');
  6.        if (target.size()) {
  7.            target.ScrollTo(400);
  8.            return false;
  9.        }
  10.     };
  11.   });
  12. });

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:

JavaScript:
  1. $(document).ready(function() {
  2.   $('a[href*=#]').click(function() {
  3.     if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'')
  4.       && location.hostname == this.hostname) {
  5.        var $target = $(this.hash);
  6.        $target = $target.length && $target || $('[name=' + this.hash.slice(1) +']');
  7.        if ($target.length) {
  8.            $target.ScrollTo(400);
  9.            return false;
  10.        }
  11.     };
  12.   });
  13. });

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]

comment feed

40 comments

  1. Hi~ Karl. Thanks your usefull plugin. I love it.

    But, your animated-scroll-plugin has crash tabs-plugin.

    How do I using both plugin?
    or How to animated-scroll-plugin's avoid some anchor?

    Best regards.

  2. Hooney, sorry that you're having trouble combining ScrollTo and Tabs. The easiest/quickest way to fix it probably would be to make the initial selector more specific. So, change this: $('a[@href*=#]'). Maybe only select links that are inside the tabs? Something like this (depending on your markup): $('div.tab-contents a[@href*=#]').

  3. Karl. Thanks your comments. but I don't understand your propose... very sorry.

    My code incorrectly run to avoid tabs's anchor scroll. See my code. plz~

    $(document).ready(function() {
      $('a[@href*=#]').click(function() {
    		if ($('a[@href*=#]') == $('div#readMore a[@href*=#]')) {
          return false;
    		} else {
        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(800);
               return false;
           }
    		};
        };
      });
    });
    

    Best regards.

  4. Hmm. The problem is definitely with this line:
    if ($('a[@href*=#]') == $('div#readMore a[@href*=#]')) {
    Maybe you mean to do something like this:
    if (this.hash == '#readMore') {

    Or, you could remove that whole if condition by making your selector more specific. Something like this in line 2:
    $('a[@href*=#]').not('a[@href$=readMore]').click(function() {

    That should find all links that have "#" in their href, excluding all of those with an href that ends in "readMore".

    Hope that helps.

  5. Thanks~ Karl. Your Kinds make my site Good UI effect!

    You are genius. Thanks very much! :)

  6. Hi Karl,

    thanks for the script, but I'm detecting some weird behaviour.

    I'm trying to use it for horizontal scrolling. For testing purposes I stripped the html-code from any other anchors leaving only 3 to scroll to.

    Now, when I click "link1" it scrolls smoothly to "section2", but when I then click "link2", instead of taking me to "section3" you can watch the scrollbar jumping back to "section 1" and then smoothly scrolling to "section2" - again. I checked the links a dozen times and simplified the code to recognize my mistake. I can't figure it out.

    Doesn't it work with horizontal scrolling?

    I'm using it with jquery 1.1.4 and your example code

    $(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(2000);
    return false;
    }
    };
    });
    });

  7. I didn't account for horizontal scrolling in the blog entry, but it looks like the .ScrollTo() method has an "axis" option built in. Try replacing line 9 with this:

    $target.ScrollTo({duration: 2000, axis: 'horizontal'})

  8. Thanks for the help Karl. Sorry to say though, it didn't work. Still the same behaviour.
    2 things I've noticed: One is that the scrolling will always start at the first position, even if that means jumping back. The other is that if I diminish the width of the sections, I'll manage to jump to more than just the 2 previously mentioned sections. There seems to be some kind of barrier at something between 2000 and 3000px.

    Yesterday I decided to use Mootools' SmoothScroll in combination with jQuery instead. Although I don't like the idea, it works (as long as you import the jQuery lib before Mootools). Still, I'd prefer an all jQuery solution.

  9. Sorry it's not working for you. I wish I could offer you more help, but I'm just not well-versed enough in the Interface methods to provide a fix or workaround.

    Beginning with jQuery 1.2, you will be able to animate scrollTo using jQuery core and the Dimensions plugin. I'll post about that when it comes out.

    There was a site I saw a while back that had some really cool horizontal animation. It didn't use scrolling, but instead had divs set to overflow: hidden and animating either the left or the margin-left property. If I can remember where that site is, I'll post a link, in case it helps you achieve the effect, albeit in a slightly different way.

    Anyway, I'm glad you managed to get things working, even if you had to use multiple libraries. MooTools does a fantastic job with animations, that's for sure.

  10. No problem. I have a working solution.
    Can't wait to fool around with jquery 1.2. Thanks again.

  11. Thanks for this code. It works great, bu I have a problem.
    I want to scroll to for example 30px before or after the id.
    Do you have any tip for me, how can I do it?
    Sorry for my English.

  12. hey men this looks great... but can i ask something can i do this or i mean can i slow down the speed and bounce back a little when it reach the top?

    tnx for this....

  13. @Dorian, I don't know if that can be done with the current implementation. It should be pretty easy to do with jQuery 1.2 and Dimensions, though. Stay tuned — it should be available within days.

    @Ronald, you can slow down the speed by changing the number inside the .ScrollTo() method. That integer represents the number of milliseconds. So, increase it to .ScrollTo(1000) to make the scrolling take a full second. I believe you can make it bounce at the end by including an easing plugin and then changing the parameters like so: .ScrollTo({duration: 1200, easing: 'bounceout'}).

  14. Hi Karl!
    Congratulations for sharing this pretty solution.

    Today, I updated the jQuery library to 1.2 and the code on "scrollto.js" started showing this error (jQuery.dequeue is not a function).
    There's a simple solution to solve this?

    Thanks!

  15. Hi Renato,
    I worked on it tonight for a while without much success. I got rid of the error, but it still wasn't working right (only scrolled on the first click for each target). I'll try to put together a version using 1.2 and the Dimensions plugin soon.

  16. Steve Ruiz

    @Renato & Karl:

    on line 72 of scrollto.js replace:

    z.clear = function(){clearInterval(z.timer);z.timer=null;jQuery.dequeue(z.e, 'interfaceFX');};

    with:

    z.clear = function(){clearInterval(z.timer);z.timer=null;jQuery(e).dequeue('interfaceFX');};

    and that should clear things up.

  17. Hi Steve,

    Thanks for the suggestion. I tried that, though, and had the problem of it only working the first time per target, as I mentioned above.

  18. Steve Ruiz

    Karl,

    Not sure but I had the one click problem as well, and this fixed it perfectly for me. Might want to give it another go with a quick copy+paste?

  19. Luke Scammell

    Hi Karl,

    I look forward to the 1.2 version, without interface, with great anticipation. Would be nice to finally ditch the moo.fx version once and for all. The interface version always was a little temperamental for me.

    Thanks :D

  20. Any news regarding the horizontal scrolling functionality?

  21. Hi Sunset Pearl,

    Check out the entry I posted yesterday, Animated Scrolling with jQuery 1.2, for a general idea of how to do animated scrolling without Interface. Then take a look at the quick demo page that I put together for you: Scroll Left. The script is in the <head>, so just view the source.

  22. youshrin

    i have a real problem with mootools n jquery! i have build a column left using jquery(include jquery only on left column) and include it on all my pages,now on the main page i have used mootools(include mootools only in main page) to build a pop! jquery stop working on this page! please help n give suggestion of how to make both work on the page as column left is included on all pages.

  23. Hi youshrin,
    During my brief excursion into the Mootools forums, I read a few comments from the developers indicating that they weren't interested in ensuring that their library works well with others. Their stance may have changed since then, but I wouldn't count on it.

    The good news is that John Resig, creator of jQuery, is fully committed to peaceful co-existence among libraries. A good place to start would be to use jQuery.noConflict(), along with a closure for your jQuery code, as this example from the documentation site shows:

    Query.noConflict();
    (function($) {
      $(function() {
        // more code using $ as alias to jQuery
      });
    })(jQuery);
    // other code using $ as an alias to the other library

    If you're still stuck, feel free to raise the question on the jQuery Google Group.

  24. Temi

    I have a problem using this script together with jquery innerfade. when i do...the fade transition in my slideshow stops working. the image just goes blank during the interval then loads the next image.

    could there be a conflict in the $(document).ready function ?

  25. Not sure, Temi. Which version of jQuery are you using? Does the problem with innerfade go away if you remove the animated scrolling script? Could there be a conflict with innerfade and the Interface module being used for the animated scrolling?

    If it's not too much trouble, I'd suggest looking at the jQuery 1.2.1 and the jQuery Cycle plugin and the script for animated scrolling with 1.2.

  26. hero

    testing testing

  27. hero

    Sorry for the above...I was just testing the submit comment action. I am very curious to know how the comment is instantly shown after hitting submit. The whole page is not loaded at all but still the comment is stored in some sort of database. Karl I would really really appreciate if you could show a little snippet that does this?

  28. Valenluis

    wow, it looks great, i'll try it ;D ;O

  29. you have to return true if the path are different (because the url are not followed else).

    if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
    ...
    }else{

    return true;}

  30. hmm, it's not working sth with this

  31. Arindam

    Hi..I'm trying to scroll some aligned div s in html using ur code.. It deosnt seem to work .. Could u assist please?

  32. I found this quite helpful. Thanks alot.

  33. efFergyOvanowrora

    ckxjkxrlpfqcwzvowell, hi admin adn people nice forum indeed. how's life? hope it's introduce branch ;)

  34. where can I see it live?

  35. Adrian

    I tried your script. It works on firefox, webkit browser prefectly but not in IE8.

  36. Dave

    I came into problem.
    I found that it scroll only once. What I means is after scrolling to an achor,
    I use mouse scroll back to the top of the page and click the anchor link again, didn't scroll till I did page refresh.

  37. Enes Inkaya

    What if i use jquery with no conflicts ? how should i set the 'target' variable? or others?
    thanks

5 Pings

  1. [...] again, so I decided to see if I could fix what ailed it in IE. This is what they had arrived at: PLAIN TEXT [...]

  2. [...] av JavaScript och Ajax. Buildinternet.com visar 17 ensidiga presentationer som med hjälp av Animerad Scrolling visar hur man snyggt löser navigation. Dela med [...]

  3. [...] Swedberg posted this a single a whilst behind on a Learning jQuery site, as good as it is really worth a look. Of course, there have been a integrate of plugins to [...]

  4. [...] the fold content, and the scrolling involved with it isn’t a hassle on the sites below. Smooth scrolling through the use of javascript libraries is common to keep things fluid when [...]

Sorry, but comments for this entry are now closed.