This isn't a totally useless effect.
It might seem like nothing but eye candy, but it can be a great way to lead a user around a page while still giving them some context as to where they are. Clicking on an anchor link that jumps straight to another part of the page can be jarring and disorienting. Animating the scroll shows clearly where they came from and how to get back there. It's also a great example of how to create private methods and members in Javascript objects. See the code in action on the example page.
The fundamental idea is simple: using window.scrollTo(x, y), it's possible
to jump the scroll bar to any point on the page. If we were to jump a couple of pixels at a time, over
2 or 3 seconds, it would look like the window was being scrolled through. Couple that with some functions
that find all links to an anchor on a page (#home, #end, etc.)
and attach an onclick event to them, and you have a flashy effect that degrades
gracefully on browsers that don't have Javascript enabled. Non-JS users still get the normal anchor links, and
everyone else gets the scrolling animation.
An Object Template
A quick look at a generic Javascript singleton object before we get into the scroller:
YourNameSpace.ObjectName = function () {var privateMember1 = 'This is a private member of YourNameSpace.ObjectName',privateMember2 = 100;var running = false;function privateMethod(argument1, argument2) {// This method can only be accessed from other TF.ObjectName methods.// It has full access to the private member variables.privateMember2 = 2000;}// Public methods are returned below.return {publicMember1: 'This is a public member of YourNameSpace.ObjectName',privledgedMethod: function(argument1) {// This method can be accessed from outside the object, but// still can read and modify the private member variables.privateMethod();console.log(privateMember2);privateMember2 = argument1;console.log(privateMember2);},publicMethod: function(argument1, argument2) {// This method can be accessed from outside the object.// Since it does not access any private variables or functions,// it is considered to be public, not privledged.console.log(this.publicMember1);}}} ();YourNameSpace.ObjectName.publicMember1 = 'I can change this variable';YourNameSpace.ObjectName.privledgedMethod(50);
YourNameSpace.ObjectName = function () {
var privateMember1 = 'This is a private member of YourNameSpace.ObjectName',
privateMember2 = 100;
var running = false;
function privateMethod(argument1, argument2) {
// This method can only be accessed from other TF.ObjectName methods.
// It has full access to the private member variables.
privateMember2 = 2000;
}
// Public methods are returned below.
return {
publicMember1: 'This is a public member of YourNameSpace.ObjectName',
privledgedMethod: function(argument1) {
// This method can be accessed from outside the object, but
// still can read and modify the private member variables.
privateMethod();
console.log(privateMember2);
privateMember2 = argument1;
console.log(privateMember2);
},
publicMethod: function(argument1, argument2) {
// This method can be accessed from outside the object.
// Since it does not access any private variables or functions,
// it is considered to be public, not privledged.
console.log(this.publicMember1);
}
}
} ();
YourNameSpace.ObjectName.publicMember1 = 'I can change this variable';
YourNameSpace.ObjectName.privledgedMethod(50);
This assumes that you have created an empty object to use as your namespace (something like var YourNameSpace = window.YourNameSpace || {};).
It's usually a good idea to do this; it gives you a place to put your code without causing potential conflicts with other variables or functions.
Global variables are evil.
In the code example above we used a function to encapsulate some variables and functions. The last line is very important: the (); at the end
means we are executing this function immediately. This is equivalent to something like var myInt = function() { return 5 * 7; }();. The function is
executed right away and the result is returned to myInt. In Javascript only functions have scope. The
only way we have to enforce private members and methods is by wrapping them in a function. The public methods are returned as an object to
YourNameSpace.ObjectName and can be
accessed like any public method (e.g. YourNameSpace.ObjectName.publicMethod()). The private methods and member variables still exist (through a
property known as closure) but are not externally accessible.
Don't worry if that didn't make much sense. I blame myself.
On to the scroller!
We're going to work through the TF.Scroller object from the bottom up. You can skip to the end for the complete code.
/* Attach the scrolling method to the links with the class 'scrolling-link'. */init: function() {var links = YAHOO.util.Dom.getElementsByClassName('scrolling-link', 'a');YAHOO.util.Event.addListener(links, 'click', TF.Scroller.anchorScroll, TF.Scroller, true);}}} ();YAHOO.util.Event.onAvailable('doc', TF.Scroller.init, TF.Scroller, true);
/* Attach the scrolling method to the links with the class 'scrolling-link'. */
init: function() {
var links = YAHOO.util.Dom.getElementsByClassName('scrolling-link', 'a');
YAHOO.util.Event.addListener(links, 'click', TF.Scroller.anchorScroll, TF.Scroller, true);
}
}
} ();
YAHOO.util.Event.onAvailable('doc', TF.Scroller.init, TF.Scroller, true);
Line 4 uses a little YUI magic to get references to all of the links with the class scrolling-link. The next
line attaches event listeners to those links. TF.Scroller.anchorScroll will now be run each time one of these links is clicked. Line 9 fires off the init() function as soon as the element with id doc available in the document.
/* Sets up and calls scrollStep. */anchorScroll: function(e, obj) {var clickedLink = YAHOO.util.Event.getTarget(e);var anchorId = clickedLink.href.replace(/^.*#/, '');var target = YAHOO.util.Dom.get(anchorId);if(target) {YAHOO.util.Event.stopEvent(e);running = true;var yCoord = ((YAHOO.util.Dom.getY(target) - 6) < 0) ? 0 : YAHOO.util.Dom.getY(target) - 6;var currentYPosition = (document.all) ? document.body.scrollTop : window.pageYOffset;var down = true;if(currentYPosition > yCoord) {stepIncrement *= -1;// Reverse the direction if we are scrolling up.down = false;}// Stop the scroll once the time limit is reached.TF.Scroller.killTimeout = window.setTimeout(TF.Scroller.killScroll, limit);// Start the scroll by calling scrollStep().scrollStep(currentYPosition + stepIncrement, yCoord, down);}},
/* Sets up and calls scrollStep. */
anchorScroll: function(e, obj) {
var clickedLink = YAHOO.util.Event.getTarget(e);
var anchorId = clickedLink.href.replace(/^.*#/, '');
var target = YAHOO.util.Dom.get(anchorId);
if(target) {
YAHOO.util.Event.stopEvent(e);
running = true;
var yCoord = ((YAHOO.util.Dom.getY(target) - 6) < 0) ? 0 : YAHOO.util.Dom.getY(target) - 6;
var currentYPosition = (document.all) ? document.body.scrollTop : window.pageYOffset;
var down = true;
if(currentYPosition > yCoord) {
stepIncrement *= -1; // Reverse the direction if we are scrolling up.
down = false;
}
// Stop the scroll once the time limit is reached.
TF.Scroller.killTimeout = window.setTimeout(TF.Scroller.killScroll, limit);
// Start the scroll by calling scrollStep().
scrollStep(currentYPosition + stepIncrement, yCoord, down);
}
},
anchorScroll() determines what link was clicked, where that link was pointing to, and if the link's destination element exists. If it does, it gets the y coordinate of the element we are scrolling to and passes the information onto the private function scrollStep(), which actually does all of the work. anchorScroll()
is just a public facing method to perform the initial setup.
Line 19 deserves a closer look. We are running the function TF.Scroller.killScroll() 6 seconds after we start the scroll. It is possible that the scroll could never end, due to a user maually moving the scroll bar, so we make sure that our automatic scroll ends after a set amount of time. 6 seconds is long enough for a 1000+ pixel scroll to finish, but you can always change this value by editting the private variable limit. We assign this timeout to the variable TF.Scroller.killTimeout so that we can stop it from executing if the scroll ends before the 6 second time limit.
/* Recursive scrolling method. Steps through the complete scroll. */function scrollStep(to, dest, down) {if(!running || (down && to >= dest) || (!down && to <= dest)) {TF.Scroller.killScroll();return;}if((down && to >= (dest - (2 * stepIncrement))) ||(!down && to <= (dest - (2 * stepIncrement)))) {stepIncrement = stepIncrement * .55;}window.scrollTo(0, to);// Assign the returned function to a public method.TF.Scroller.nextStep = callNext(+to + stepIncrement, dest, down);window.setTimeout(TF.Scroller.nextStep, stepDelay);}/* Create a closure so that scrollStep can be accessed by window.setTimeout(). */function callNext(to, dest, down) {return function() { scrollStep(to, dest, down); };}
/* Recursive scrolling method. Steps through the complete scroll. */
function scrollStep(to, dest, down) {
if(!running || (down && to >= dest) || (!down && to <= dest)) {
TF.Scroller.killScroll();
return;
}
if((down && to >= (dest - (2 * stepIncrement))) ||
(!down && to <= (dest - (2 * stepIncrement)))) {
stepIncrement = stepIncrement * .55;
}
window.scrollTo(0, to);
// Assign the returned function to a public method.
TF.Scroller.nextStep = callNext(+to + stepIncrement, dest, down);
window.setTimeout(TF.Scroller.nextStep, stepDelay);
}
/* Create a closure so that scrollStep can be accessed by window.setTimeout(). */
function callNext(to, dest, down) {
return function() { scrollStep(to, dest, down); };
}
The last part of the TF.Scroller object is the scrollStep() function. Line 12 performs the actual scroll, while lines 14 and 15 set up the innvocation of this function. Because scrollStep() is a private function, we cannot set a timeout on it. Instead, we use the callNext() function to create a closure that allows timeout to access a public method, which in turn accesses the private method.
The Final Product
There you have it, the full object. Include this in your page (along with yahoo.js, event.js and dom.js from the YUI)
and any anchor link with the class scrolling-link will be animated. You can see exactly how it's implemented on the example page.
var TF = window.TF || {};/* -------------------------------------------------------------------------- *//* Functions to initialize and perform the scrolling anchor links. *//* -------------------------------------------------------------------------- */TF.Scroller = function () {var stepIncrement = 50;// The number of pixels that each step moves the window.var stepDelay = 10;// The number of milliseconds between steps.var limit = 6 * 1000;// After 6 seconds the scroll is killed.var running = false;/* Recursive scrolling method. Steps through the complete scroll. */function scrollStep(to, dest, down) {if(!running || (down && to >= dest) || (!down && to <= dest)) {TF.Scroller.killScroll();return;}if((down && to >= (dest - (2 * stepIncrement))) ||(!down && to <= (dest - (2 * stepIncrement)))) {stepIncrement = stepIncrement * .55;}window.scrollTo(0, to);// Assign the returned function to a public method.TF.Scroller.nextStep = callNext(+to + stepIncrement, dest, down);window.setTimeout(TF.Scroller.nextStep, stepDelay);}/* Create a closure so that scrollStep can be accessed by window.setTimeout(). */function callNext(to, dest, down) {return function() { scrollStep(to, dest, down); };}return {nextStep: null,killTimeout: null,/* Sets up and calls scrollStep. *//* Sets up and calls scrollStep. */anchorScroll: function(e, obj) {var clickedLink = YAHOO.util.Event.getTarget(e);var anchorId = clickedLink.href.replace(/^.*#/, '');var target = YAHOO.util.Dom.get(anchorId);if(target) {YAHOO.util.Event.stopEvent(e);running = true;var yCoord = ((YAHOO.util.Dom.getY(target) - 6) < 0) ? 0 : YAHOO.util.Dom.getY(target) - 6;var currentYPosition = (document.all) ? document.body.scrollTop : window.pageYOffset;var down = true;if(currentYPosition > yCoord) {stepIncrement *= -1;// Reverse the direction if we are scrolling up.down = false;}// Stop the scroll once the time limit is reached.TF.Scroller.killTimeout = window.setTimeout(TF.Scroller.killScroll, limit);// Start the scroll by calling scrollStep().scrollStep(currentYPosition + stepIncrement, yCoord, down);}},/* Kill the scroll after a timeout, to prevent an endless loop. */killScroll: function() {window.clearTimeout(TF.Scroller.killTimeout);running = false;stepIncrement = 50;},/* Attach the scrolling method to the links with the class 'scrolling-link'. */init: function() {var links = YAHOO.util.Dom.getElementsByClassName('scrolling-link', 'a');YAHOO.util.Event.addListener(links, 'click', TF.Scroller.anchorScroll, TF.Scroller, true);}}} ();YAHOO.util.Event.onAvailable('doc', TF.Scroller.init, TF.Scroller, true);
var TF = window.TF || {};
/* -------------------------------------------------------------------------- */
/* Functions to initialize and perform the scrolling anchor links. */
/* -------------------------------------------------------------------------- */
TF.Scroller = function () {
var stepIncrement = 50; // The number of pixels that each step moves the window.
var stepDelay = 10; // The number of milliseconds between steps.
var limit = 6 * 1000; // After 6 seconds the scroll is killed.
var running = false;
/* Recursive scrolling method. Steps through the complete scroll. */
function scrollStep(to, dest, down) {
if(!running || (down && to >= dest) || (!down && to <= dest)) {
TF.Scroller.killScroll();
return;
}
if((down && to >= (dest - (2 * stepIncrement))) ||
(!down && to <= (dest - (2 * stepIncrement)))) {
stepIncrement = stepIncrement * .55;
}
window.scrollTo(0, to);
// Assign the returned function to a public method.
TF.Scroller.nextStep = callNext(+to + stepIncrement, dest, down);
window.setTimeout(TF.Scroller.nextStep, stepDelay);
}
/* Create a closure so that scrollStep can be accessed by window.setTimeout(). */
function callNext(to, dest, down) {
return function() { scrollStep(to, dest, down); };
}
return {
nextStep: null,
killTimeout: null,
/* Sets up and calls scrollStep. */
/* Sets up and calls scrollStep. */
anchorScroll: function(e, obj) {
var clickedLink = YAHOO.util.Event.getTarget(e);
var anchorId = clickedLink.href.replace(/^.*#/, '');
var target = YAHOO.util.Dom.get(anchorId);
if(target) {
YAHOO.util.Event.stopEvent(e);
running = true;
var yCoord = ((YAHOO.util.Dom.getY(target) - 6) < 0) ? 0 : YAHOO.util.Dom.getY(target) - 6;
var currentYPosition = (document.all) ? document.body.scrollTop : window.pageYOffset;
var down = true;
if(currentYPosition > yCoord) {
stepIncrement *= -1; // Reverse the direction if we are scrolling up.
down = false;
}
// Stop the scroll once the time limit is reached.
TF.Scroller.killTimeout = window.setTimeout(TF.Scroller.killScroll, limit);
// Start the scroll by calling scrollStep().
scrollStep(currentYPosition + stepIncrement, yCoord, down);
}
},
/* Kill the scroll after a timeout, to prevent an endless loop. */
killScroll: function() {
window.clearTimeout(TF.Scroller.killTimeout);
running = false;
stepIncrement = 50;
},
/* Attach the scrolling method to the links with the class 'scrolling-link'. */
init: function() {
var links = YAHOO.util.Dom.getElementsByClassName('scrolling-link', 'a');
YAHOO.util.Event.addListener(links, 'click', TF.Scroller.anchorScroll, TF.Scroller, true);
}
}
} ();
YAHOO.util.Event.onAvailable('doc', TF.Scroller.init, TF.Scroller, true);
Comments
Alex:Tuesday, August 15th, 2006 at 1:05 #1
Why not skip all this and just use moo.fx? I mean, in terms of practicality. Your solution, of course, is a great illustration of how to achieve a similar effect using Yahoo's JS library. =)
I see some interesting uses for this in terms of navigating horizontally within a page. Or just some extra eye-candy for scrolling thru long help files. ;)
Dave:Tuesday, August 15th, 2006 at 4:30 #2
Yeah great job with this. Its nice to see someone actually apply the yahoo libs for once, but moofx were here first with quite some considerable head-start. It just seems churlish to replicate work done in far less js, ages ago.
james:Tuesday, August 15th, 2006 at 10:05 #3
I can understand someone looking at the moo.fx code and not wanting to run it on a system for production.
chris:Tuesday, August 15th, 2006 at 10:54 #4
How about jquery and the fx interface plugin (about 30k in js files)
$('a[@href^="#"]').each(function(i){
$(this).click(function(){
$(this.href).ScrollTo(2000);
return false;
});
});
Ross:Tuesday, August 15th, 2006 at 12:43 #5
The YUI libs are used here to eliminate a lot of cross-browser headaches. Functions like
YAHOO.util.Event.addListenerandYAHOO.util.Dom.getYprovide a way to do with one line of code something that would normally take several lines for each browser, plus the conditional logic to separate them. I prefer a stable, production ready library that gives you the tools you need to code javscript in a relatively browser neutral environment, not one that gives you Shiny.Effect.1 and Crazy.Animation.7 at the price of a 100k source file. There is nothing wrong with effects and animations (I'm generally a fan of both when it conveys to the user that an action is taking place), I just like more control and flexibility.Dustin Diaz:Tuesday, August 15th, 2006 at 13:17 #6
Indeed it comes down to how much control the developer wants as Ross mentioned. A prototype + scriptaculous implementation is nice because you get bundled effects with minimal effort in implementing them however it comes at the price of large file sizes... but that devils advocacy has long been talked about. The fact of the mater is, libraries like jQuery, Prototype or moo.fx can't compare to YUI since they're virtually aiming at two different goals. YUI isn't a framework. There are some core widgets but the main bulk of it are the utilities that aid cross browser development.
This animated page demonstration is a good example of showing the differences. Chris' comment clearly shows the power of jQuery and the fx plugin, but with the YUI Anim utility, you can virtually do any animation you want - and with a much more amazing cross-browser compatibility ratio. Essentially it gives you back the power of being the developer, and not just an implementer of dropping in canned macros.
Old Crabby Crab Apple(PC):Tuesday, August 15th, 2006 at 14:26 #7
Jump to paragraph one!
Paragraph One
All that JS for what?
Jump to paragraph two!
Make way for marginal convenience.
Beauty becomes boredom in a sea of the abundant unique.
Loup-Vert:Tuesday, August 15th, 2006 at 16:10 #8
I have one issue with this rewriting of pages' intralinks. Some people use the #newpageloc appendix of the URL to keep track of where in a page they are. This script removes the addition of the #newpageloc to the URL, which reduces preservative accessibility (bookmarking, history navigation).
I'd suggest a location.href update once the scrolling coolness has concluded, but I'm unsure if there's an appropriately pollable return code coming from this iterating animation.
Of course, this could lead users to think, "It looks awesome going forward, but clicking 'back' isn't as cool! Inconsistent!" Oh well.
Abba Bryant:Tuesday, August 15th, 2006 at 20:10 #9
Jquery can do this as well as YUI - with less overhead, less code. I don't believe jquery is a 'framework' as much as prototype / scriptaculous - it does what yui does - provides a set of tools to manipulate the dom and set / retrieve properties and attributes. If it was an fx lib the interface plugins wouldn't be needed for anything more than hide, show, fade and the basics.
YUI is sexy though - even though renamespacing my YUI code into something shorter every time I work with the libs is a pain.
Dustin Diaz:Tuesday, August 15th, 2006 at 23:48 #10
@Abba: as noted in a previous comment, this can indeed be done with jQuery. The code is even written in comment #4. The namespacing issue is by far, and still, the weakest argument against YUI and I believe that it actually makes it a more professional library. It safe guards your web applications and it is very-very unlikely that it will collide with any existing code. If you truly cared about the verbosity, you can create your own shortcuts with minimal code. Eg:
var $D = YAHOO.util.Dom;
var $E = YAHOO.util.Event;
Aside from it all, none of this is to take away the excellent tutorial given by Ross. This can actually be implemented without YUI or even any other JavaScript helper library. Ross just chose YUI as a simple way to assign event listeners and grab Y cordinates on a page. Since the total combined file size of Event + Dom is only ~ 5k or so, there's no guilty feeling of using it only for just a handful of functions. It got the job done and it did it well.
Richard:Wednesday, August 16th, 2006 at 20:36 #11
Is it me or is this script not actually scrolling to the new point FROM the position of the click point? Why does the script have to start at the top of the page all the time.
I remembers somewhere seeing a script that moves from where the person is clicking to the new point fluidly - wish I could remember where it was...
Gerry Quach:Wednesday, August 16th, 2006 at 20:56 #12
Great script! I couldn't get some parts of the example to work in Firefox 1.5 and Opera 9 though. The first link (the scrolling to paragraph 2) works fine, but all the rest of the scrolls end up being jumps instead.
Am I missing something obvious?
Ross:Friday, August 18th, 2006 at 1:20 #13
Richard:
The script should scroll from your current location on the page to the new location. It shouldn't move to the top of the page first. What browser are you using that you see that behavior?
Gerry:
I tested the example in Firefox 1.5.0.6 (Mac and Windows) and got it to work. Are you using a slightly out-of-date version? Do certain links (e.g. 3 and 4) not animate properly, or do all links clicked after the first one fail?
Raul BC:Friday, August 18th, 2006 at 6:43 #14
I'm with Richard, tested the example code with IE 6.0.2900.2180..... and the script do right if we have to move down, but goes to the top of the page first if we want to go up.
Tested in same machine with Firefox and the result is perfedt!
Great script, I'll use it anyway :)
Ross:Friday, August 18th, 2006 at 21:20 #15
Thanks for the info, Raul. I'll do some testing and see if I can fix that.
zack:Wednesday, August 23rd, 2006 at 2:21 #16
ok, no one said it yet, i don't think, but please remember:
Javascript should be an enhancement, never to rely on it for site functionality!
(Testing this w/ js disabled - the anchors did not work at all)
I didn't review the code too much, and hey maybe it's just MY machine-
but the moral of the story to everyone is that noteworthy examples from talented young men should at least follow some basic web standard principles!
It of course can be done and I just think all js examples should be implemented this way- or else javascript will die a second death...
too many people copy and paste your code, so to everyone:
please, only publish true and tried bulletproof examples. That's good tasty JS-
web 2.0 is on it's frail unsteady legs and it begs you... client script wisely!
stylemo:Thursday, August 24th, 2006 at 10:47 #17
I'd still use moofx for this type of scrolling animation. Why? because it's unobtrusive. Anchor links still work when the moofx is bypassed.
But I'd still say great work and implementation on the YUI. Although I wouldn't want to use YUI anyway. It's got a lot of files.
Ross:Monday, August 28th, 2006 at 2:26 #18
zack: I've tested this with Javascript disabled (and I tried it again right now), the links work just fine. If there is no JS, then the onclick events are never attached and the anchor links take you to the correct section as they normally would.
The whole point is that is unobtrusive; folks with JS enabled get the animation, everyone else gets the same functionality, but no flashy effect.
stylemo: The same is true of this script. And the whole point of the YUI is that is has a lot files. Modularity lets you use only the libraries you need, decreasing the size of the JS you have to load.
zoel:Wednesday, August 30th, 2006 at 12:06 #19
hmm, how about moofx JS, same ? i think Moofx so soft when scrolling the page :-)
elv0:Wednesday, September 6th, 2006 at 4:58 #20
do you have a demo?
shajed:Thursday, September 28th, 2006 at 3:58 #21
Great script!
Is it posible to scroll bookmark to bookmark auto without click?
Gustavo:Friday, October 6th, 2006 at 17:00 #22
I like the way the script works, however, i'm having trouble implementing it in my current page. I'm using asp 2.0 master pages with a fixed header and footer. unfortunately the scrolling doesnt work at all... the links work and the page moves to the correct anchor/div but without the effect. do you know if there is a problem trying to scroll within a div? Basically the body doesnt scroll but the inner div that contains the content does.
Thanks!
Liam:Sunday, November 19th, 2006 at 7:01 #23
If this were to be used by a user that has text enlarged it wouldnt work. you could do with altering it so it dosnt use x/y co-ords to get to the desired position.
i just tested in firefox and enlarged the text by say 3 or 4 times. and when i clicked the links it diddnt scroll to the right paragraph.
Aaron Schmidt:Thursday, November 30th, 2006 at 15:20 #24
Besides the great scrolling tutorial, I really appreciate the time you spent on describing the object template. I've recently switched from using Prototype to the YUI library and I've been looking for this type of information. I've gone through all of Dustin Diaz's screencasts which often incorporate this Javascript development pattern so it's nice to get a basic overview to tie everything together.
Seems to be a lot of Moo.fx fan boys commenting on your article. I was using Moo.fx before I switched to YUI (and your scrolling function). I don't have anything against it but I found it a little frustrating to have to include Prototype for my framework, Scriptaculous for my affects and Moo for scrolling ... just seemed so disjointed.
js_newbe:Friday, December 1st, 2006 at 11:33 #25
Hi, I am new to js I tried to use this. but I am confused which part of the script to use.
I tried to copy section after section of this tutorial and put them inside a javascript tag on the head (of course after YAHOO js files.) but my page with links (and scrolling-link class) does not slide. It jumps to the target on firefox like an ordinary named-link j. but not slides. can someone explain this?
thanks
fsClock:Monday, December 4th, 2006 at 11:27 #26
Hi
Has anyobody used this with frames?