Introducing MooTools ScrollSpy

By  on  

I've been excited to release this plugin for a long time. MooTools ScrollSpy is a unique but simple MooTools plugin that listens to page scrolling and fires events based on where the user has scrolled to in the page. Now you can fire specific functionality with just a few simple parameters.

The MooTools JavaScript Class

/* scroll spy plugin / class */
var ScrollSpy = new Class({
	
	/* implements */
	Implements: [Options,Events],

	/* options */
	options: {
		min: 0,
		mode: 'vertical',
		max: 0,
		container: window,
		onEnter: $empty,
		onLeave: $empty,
		onTick: $empty
	},
	
	/* initialization */
	initialize: function(options) {
		/* set options */
		this.setOptions(options);
		this.container = $(this.options.container);
		this.enters = this.leaves = 0;
		this.max = this.options.max;
		
		/* fix max */
		if(this.max == 0) 
		{ 
			var ss = this.container.getScrollSize();
			this.options.max = this.options.mode == 'vertical' ? ss.y : ss.x;
		}
		/* make it happen */
		this.addListener();
	},
	
	/* a method that does whatever you want */
	addListener: function() {
		/* state trackers */
		this.inside = false;
		this.container.addEvent('scroll',function() {
			/* if it has reached the level */
			var position = this.container.getScroll();
			var xy = this.options.mode == 'vertical' ? position.y : position.x;
			/* if we reach the minimum and are still below the max... */
			if(xy >= this.options.min && xy ≪= this.max) {
					/* trigger Enter event if necessary */
					if(!this.inside) {
						/* record as inside */
						this.inside = true;
						this.enters++;
						/* fire enter event */
						this.fireEvent('enter',[position,this.enters]);
					}
					/* trigger the "tick", always */
					this.fireEvent('tick',[position,this.inside,this.enters,this.leaves]);
			}
			else {
				/* trigger leave */
				if(this.inside) 
				{
					this.inside = false;
					this.leaves++;
					this.fireEvent('leave',[position,this.leaves]);
				}
			}
		}.bind(this));
	}
});

Options for ScrollSpy include:

  • min: (defaults to 0) The minimum value of the X or Y coordinate, depending on mode.
  • max: (defaults to 0) The maximum value of the X or Y coordinate, depending on mode.
  • mode: (defaults to 'vertical') Defines whether to listen to X or Y scrolling.
  • container: (defaults to window) The element whose scrolling to listen to.

Events for ScrollSpy include:

  • Tick: Fires on each scroll event within the min and max parameters. Receives as parameters:
    • position: an object with the current X and Y position.
    • inside: a Boolean value for whether or not the user is within the min and max parameters
    • enters: the number of times the min / max has been entered.
    • leaves: the number of times the min / max has been left.
  • Enter: Fires every time the user enters the min / max zone.
    • position: an object with the current X and Y position.
    • enters: the number of times the min / max has been entered.
  • Leave: Fires every time the user leaves the min / max zone.
    • position: an object with the current X and Y position.
    • leaves: the number of times the min / max has been left.

So now that we have the basics down, lets check out some example usages.

Example 1: "Top the Top"

In this example, when you scroll down a defined number of pixels, you get a "Scroll to Top" link in the lower right hand part of the screen. When you're back at the top, ScrollSpy is directed to hide the link.

The XHTML

<a href="#top" id="gototop" class="no-click no-print">Top of Page</a>

The CSS

#gototop			{ display:none; font-weight:bold; font-family:tahoma; font-size:10px; width:70px; background:url(/wp-content/themes/walshbook/images/add_content_spr.gif) 5px -8px no-repeat #eceff5; color:#3b5998; font-size:11px; text-decoration:none; position:fixed; right:5px; bottom:5px; padding:7px 7px 7px 20px; }
#gototop:hover	{ text-decoration:underline; }

The MooTools / ScrollSpy JavaScript

window.addEvent('domready',function() {
	/* smooth */
	new SmoothScroll({duration:500});
	
	/* link management */
	$('gototop').set('opacity','0').setStyle('display','block');
	
	/* scrollspy instance */
	var ss = new ScrollSpy({
		min: 200,
		onEnter: function(position,state,enters) {
			$('gototop').fade('in');
		},
		onLeave: function(position,state,leaves) {
			$('gototop').fade('out');
		},
		container: window
	});
});

Example 2: "The Show"

When you click the link in this example, the window scrolls to the right. During the scrolling process, ScrollSpy shows and hides content blocks based on where in the scrolling process the window is.

The XHTML

<!-- SLIDER 1 -->
<div style="position:relative; height:400px;">
<div id="slider1" class="slider" style="margin-left:1000px;">
	<h2>ScrollSpy!</h2>
	<p>
		ScrollSpy is a new plugin that watches where you scroll and allows
		you to perform actions based on the the position of the given
		element!
	</p>
</div>
<!-- SLIDER 2 -->
<div id="slider2" class="slider" style="margin-left:1600px;">
	<h2>ScrollSpy 2!</h2>
	<p>
		Another area!
	</p>
</div>
<!-- SLIDER 3 -->
<div id="slider3" class="slider" style="margin-left:2200px;">
	<h2>ScrollSpy 3!</h2>
	<p>
		You've met another criteria!!
	</p>
</div>
<!-- SLIDER 4 -->
<div id="slider4" class="slider" style="margin-left:2800px;">
	<h2>ScrollSpy 4!</h2>
	<p>
		You've met the last criteria!!
	</p>
</div>
<div style="clear:both;"></div>
</div>
<!-- RIGHT-MOST AREA -->
<div style="float:right;" id="right2"> </div>

The CSS

.slider { padding:10px; background:#eee; width:300px; height:300px; float:left; z-index:500; }

The MooTools / ScrollSpy JavaScript

window.addEvent('domready',function() {
	
	/* sliders */
	var slide1 = new Fx.Slide('slider1',{
		duration: 400,
		wheelStops: false
	});
	slide1.hide();
	var slide2 = new Fx.Slide('slider2',{
		duration: 400,
		wheelStops: false
	});
	slide2.hide();
	var slide3 = new Fx.Slide('slider3',{
		duration: 200,
		wheelStops: false
	});
	slide3.hide();
	var slide4 = new Fx.Slide('slider4',{
		duration: 200,
		wheelStops: false
	});
	slide4.hide();
	
	/* scrollspy instance */
	var ss1 = new ScrollSpy({
		min: 400,
		max: 700,
		onEnter: function(position,state,enters) {
			slide1.slideIn();
		},
		onLeave: function(position,state,leaves) {
			slide1.slideOut();
		},
		container: window,
		mode: 'horizontal'
	});
	
	/* scrollspy instance */
	var ss2 = new ScrollSpy({
		min: 900,
		max: 1500,
		onEnter: function(position,state,enters) {
			slide2.slideIn();
		},
		onLeave: function(position,state,leaves) {
			slide2.slideOut();
		},
		container: window,
		mode: 'horizontal'
	});
	
	/* scrollspy instance */
	var ss3 = new ScrollSpy({
		min: 1500,
		max: 2300,
		onEnter: function(position,state,enters) {
			slide3.slideIn();
		},
		onLeave: function(position,state,leaves) {
			slide3.slideOut();
		},
		container: window,
		mode: 'horizontal'
	});
	
	/* scrollspy instance */
	var ss4 = new ScrollSpy({
		min: 2200,
		max: 2500,
		onEnter: function(position,state,enters) {
			slide4.slideIn();
		},
		onLeave: function(position,state,leaves) {
			slide4.slideOut();
		},
		container: window,
		mode: 'horizontal'
	});
	
	/* left to right scroll */
	$('show2').addEvent('click',function(e) {
		e.stop();
		var to = $('right2').getPosition();
		to.y = 0; to.x = to.x - 300;
		var scroll = new Fx.Scroll(window,{
			duration: 20000,
			offset: to
		}).start();
	});
	
	
});

Example 3: "Team Colors"

This basic example displays a different background color depending on where you are in the page.

The XHTML

<div id="white" class="color"><h2>The White Nation</h2></div>
<div id="red" class="color"><h2>The Red Nation</h2></div>
<div id="blue" class="color"><h2>The Blue Nation</h2></div>
<div id="green" class="color"><h2>The Green Nation</h2></div>
<div id="black" class="color"><h2>The Black Nation</h2></div>

The CSS

.red		{ background:#f00; }
.blue		{ background:#00f; }
.green	{ background:#008000; }
.black	{ background:#000; color:#fff; }
.color	{ height:400px; }

The MooTools / ScrollSpy JavaScript

window.addEvent('domready',function() {
	var colors = $$('.color');
	colors.each(function(color,i) {
		var pos = color.getCoordinates();
		var ss = new ScrollSpy({
			min: pos.top,
			max: pos.bottom,
			onEnter: function() {
				$$('div.content').setStyle('background-color',color.get('id'));
			}
		});
	});
});

Example 4: "Position Pointer"

Starring world television actor Peter Griffin, this example displays imagery in different positions on the page based upon where the user scrolls.

The XHTML

<div class="zone">
	<h2>Area 1</h2>
	<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p><p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat.</p>
</div>

<div style="float:right;" class="zone right">
	<h2>Area 2</h2>
	<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p><p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat.</p>
</div>
<div style="clear:both;"></div>

<div class="zone">
	<h2>Area 3</h2>
	<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p><p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat.</p>
</div>

The CSS

.zone	{ width:500px; }

The MooTools / ScrollSpy JavaScript

window.addEvent('domready',function() {
	/* collect zones */
	var zones = $$('div.zone');
	var imageOffset = { x: 200, y: 50 };
	var posOffset = { x: 0, y: -150 }
	
	/* scroll spy */
	zones.each(function(zone, i) {
		var pos = zone.getCoordinates();
		var right = zone.hasClass('right');
		var peter = new Element('img',{
			opacity: 0,
			src: right ? '/demo/peter-left.jpg' : '/demo/peter-right.jpg',
			styles: {
				position: 'absolute',
				top: pos.top + imageOffset.y,
				left: right ? pos.left - imageOffset.x - 100 : pos.right + imageOffset.x
			}
		}).inject(document.body);
		
		var spy = new ScrollSpy({
			min: pos.top + posOffset.y,
			max: pos.bottom + posOffset.y,
			onEnter: function(position) {
				peter.fade('in');
			},
			onLeave: function(position) {
				peter.fade('out');
			}
		});
	});
});

ScrollSpy gives you a great amount of functionality within a small plugin. Coming soon: ScrollSpy LazyLoad and ScrollSpy LoadMore! Please share ideas and comments!

Recent Features

Incredible Demos

Discussion

  1. Hi David,

    none of your demos work here. FF3.1. (24″, 1900×1200, perhaps the problem?)

    1,#3,#4 dont show any effect, #2 scrolls very weird to the right.

  2. Fixed. Stupid console.log statements!

  3. Rodney

    Same here, none work, just the one that scrolls to the right.
    FF 3.0.1 1024×768

  4. Much better ;-)

  5. good job!

  6. jem

    This is a cool idea, but it almost seems unnecessary to create multiple instances of the class for “spying on” multiple elements on the page. I think the class could be abstracted a bit more to where one ScrollSpy instance could manage limitless amounts of targets instead of attaching tons of event listeners to the window scroll event.

    The hard part would be cataloging these min and max values for everything to where you could easily identify when to fire what event for what item…

    Something like

    myScrollSpy.addSubject(subjectId, {min: value, max: value}, customeEventNameOrFunctionCall)

  7. @Jem: I was going to do that but I ended up “dumbing down” the plugin to keep it simple. You could set the min to 0 and max to 0 (unlimited) and put all of your specific range logic within the onTick — that’s useless though. At that point, you may as well ditch this plugin and use the regular window.addEvent('scroll'); event handling.

  8. That’s pretty cool, very similar to Google Reader, Facebook etc. Any plugins like this for JQuery?

  9. Neal G: I tried a jQuery version but I was having issues getting the scroll value from the Event object. Haven’t tried in a while though.

  10. Dude, this is brilliant. Very sweet idea!

  11. coool!!!! I have been playing with mootools for 3 weeks now, and this is the best resource i found so far
    really love that ^Top of Page thing.. very useful
    anyhow, i haven’t getting around with plugins, do i have to make this myself? or should i include it like mootools-more?

  12. Love it man! With regard to @Jem’s comment (though a good idea) I think it was smart to keep its usage simple. Plugin consumers want a straight forward implementation… good job.

  13. 1 is so simple yet so effective. Thank you~

  14. Well, it took me a few minutes, but I’ve managed to reproduce these samples.
    I’ve placed samples 1, 3 & 4 together, they work as expected.
    Now to see what type of variations I can come up with.

    I can see this used to fade in adverts at just the right moment.
    Or on blogs to fade in the next excerpt as you arrive to it.
    Lower info bars appearing when you are half way down a page.
    previous / next in series as you reach the end of a page.

    I’ll be watching to see how people will use this plugin,
    thanx David
    very smooth work.

  15. Nice plugin David!!

  16. Absolutely well done

  17. Awe. Some. Thanks!

  18. Awesome, cant wait to try example #2!

  19. awesome! … well done. Hope you’re having the time (and enough motivation) to make a jquery version.

  20. IE6 fix

    html{
        background:url(../images/blankimage.gif) fixed; /* Blank image used to stop vibration */
    }
    #gototop{
        position:absolute; 
        top : expression(-this.offsetHeight+document.documentElement.clientHeight+document.documentElement.scrollTop+"px"+4);
    }
    
  21. Woah! That’s so great! Looking forward for a jQuery version!

  22. I’d like to add an extra feature on it. I’ve got a large div element that needs to be relative on top after I’ve scrolled 200px. When I mouseenter this specific div I’d like to remove the relative behaviour. When I mouseleave the div the div has to be relative again.

    How to apply this on the class?

  23. This is just perfekt for a “flip-book” effekt by scolling down the page ^^
    I like it :)

  24. very nice article…keep on the good work

  25. Great work! thanks for all script :)

  26. this information most important for me.
    thanks…

  27. I was wondering if you had any plans to update the script to work on the iphone?

  28. Awesome stuff mate, love the code!

  29. luka

    wel… I’m having some problems with Google Chrome…spyScroll works great under Firefox, but example 3 seems to be not working properly under Chrome…

  30. masted

    David, copy button copys text without new line breaks

  31. Hey David !

    Thanks a lot for this great class !

    Just a word : i’m not sure, but you wrote thoses lines :

    /* fix max */
    if(this.max == 0) 
        { 
            var ss = this.container.getScrollSize();
            this.options.max = this.options.mode == 'vertical' ? ss.y : ss.x;
        }
    

    But you do not fix this.max but only this.options.max. Therefore, shouldn’t you use this.options.max in the rest of the code, or modify you code such as :

    /* fix max */
    if(this.max == 0) 
        { 
            var ss = this.container.getScrollSize();
            this.options.max = this.options.mode == 'vertical' ? ss.y : ss.x;
            this.max = this.options.max;
        }
    
  32. This is great! there is so much potential for this. I stumbled across a site using your tool to make it so things with fixed positioning won’t be fixed anymore but absolutely positioned when scrolling to the right if the content is only supposed to scroll vertically. However his method doesn’t account for a screen that is aligned to the center, leaving me with a broken menu alignment whenever I move the scrollbar.

    This is the js

    var ss = new ScrollSpy({
         mode: 'horizontal',
         onTick: function(position,state,enters,leaves) {
              $("menu").style.left = -position.x+"px";
         },
         container: window
    });
    

    the HTML

    <!-- wrapper for content /-->
    <!-- Start Menu Wrapper /-->
    	
    		
    			<a href="index.html" title="home" rel="nofollow">RYAN</a>
    		
                                        
    			<a href="why.html" title="why" rel="nofollow">WHY</a>
    		
    	<!-- End Second set of links /-->
    <!-- End Menu Wrapper/-->
    <!-- Start Content/-->	
    
    <!-- End Content/-->
    <!-- End Content Wrap /-->
    

    the css

    #menu{
    	margin:0;
    	float:left;
    	display:block;
    	position:fixed;
    	overflow:hidden;
    }
    #content_wrap{
    			padding:34px 34px 94px 34px;
    			width:892px;
    			overflow:hidden;
    			margin:0 auto;
    		}
    
    

    how can I make it so this only starts when someone scrolls horizontally (because of a small browser window or monitor) and goes “back to normal” if the screen is big enough where there are no horizontal scroll bars because the horizontal width doesn’t go past the browser size?

  33. Love this plugin! Hey I am working on a site and would like to as you scroll down change not just the background color but also the background image is this possible using this plugin (As shown in example #3). Just wanted to see if their were some code gurus out there who know how to make this happen. THANKS!

  34. Daniel Ritter

    It doesn’t work on my website. I have mootools 1.3.5. Could somebody get it work with mootools 1.3.5?

  35. Daniel Ritter

    Thank you Simon André!

    /* fix max */
    if(this.max == 0)
        {
            var ss = this.container.getScrollSize();
            this.options.max = this.options.mode == 'vertical' ? ss.y : ss.x;
            this.max = this.options.max;
        }
    

    was the solution.

  36. Andrey

    Hello.
    Very good plugin. Thanks!
    Look please don’t work correctly function stop() in file ScrollSpy-yui-compressed.js

  37. Joe Isaacson

    This is pretty great, awesome touch/feel. This is a very Jr question, but do I have to grab the “MooTools JavaScript Class” first part of text in order to implement example 4 onto my site? Thanks!

  38. Hi David,

    Really nice plugin. Was wondering if you’re planning to make a wordpress plugin out of this. Sorry might be a noob question. But thought I’d ask! :)

  39. Luca

    Hey! The demo links are broken! :)

  40. I’m trying to get ScrollSpy working with wordpress and am trying to acheive a similar effect to example 4. How do I go about that?

  41. Jesse Fowler

    First, what a nice piece of code! I’m having an issue in Firefox 33.1 where the onEnter won’t trigger on a page with a bookmark in the url. The expected behavior will occur on a refresh but not from a different url to one with a #bookmark in it. It works perfectly in Chrome and IE.

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!