Video icon 64
Learning to code? Skill up faster with our practical video courses. Start your free trial today.
Advertisement

Spotlight: Constrained Stickies with jQuery

by

Every other week, we'll take an ultra focused look at an interesting and useful effect, plugin, hack, library or even a nifty technology. We'll then attempt to either deconstruct the code or create a fun little project with it.

Today, we're going to take a look at a plugin that implements a pretty neat effect -- it's pretty hard to explain in a sentence so you may as well click on the continue button to get started after the jump.


A Word from the Author

As web developers, we have access to a staggering amount of pre-built code, be it a tiny snippet or a full fledged framework. Unless you're doing something incredibly specific, chances are, there's already something prebuilt for you to leverage. Unfortunately, a lot of these stellar offerings languish in anonymity, specially to the non-hardcore crowd.

This series seeks to rectify this issue by introducing some truly well written, useful code -- be it a plugin, effect or a technology to the reader. Further, if it's small enough, we'll attempt to deconstruct the code and understand how it does it voodoo. If it's much larger, we'll attempt to create a mini project with it to learn the ropes and hopefully, understand how make use of it in the real world.


Introducing stickyFloat

Here is some quick info:

  • Type: Plugin
  • Technology: JavaScript [Built on the jQuery library]
  • Function: Floating content only within the constrains of its parent's boundaries
  • Plugin homepage: Here

The Problem

In many cases, you need the content to be floating as you scroll, but only within its parent.

Floating content as a user scrolls through the rest of the page is child's play. No JavaScript is necessary -- you can do it with just plain old CSS. Slap a position: fixed declaration on it and boom!, you have a container that is fixed in a specific location in the page -- it's floating in the page to be more colloquial.

But let's face it, it doesn't work with every layout. You could plan a little ahead and position it on the page so it'd never interfere with important elements but it'd neither be completely foolproof nor reusable elsewhere without extensive changes.

In these cases, you need the content to be floating as you scroll, but only within its parent.. If you're wondering, yes, this functionality is a variation of the one Andrew showed you in last week's tutorial which is how I got to know about this plugin.

As you will find in web development, much like multivariable calculus, there are a often a number of solutions to any given problem. Let's look at one of those alternate solutions.


Deconstructing the Logic

The general logic or workflow of the plug is actually pretty simple. Let me show you. Keep in mind that I'm gonna refer to the element that needs to be floated as sticky from now on.

But before we start, here's a quick mockup to show the hierarchy:

Tutorial image

The entire logic of the plugin can be watered down to:

  • Calculate the current position of the sticky element's parent, relative to the document. Marked as 1 in the image.
  • Obtain the parent's height as well - So we'll know when to stop floating when we're past the parent. Marked as 2.
  • Calculate how far the page has been scrolled down - To find out whether we are looking at the parent -- to see whether we are in range. In the image above, the horizontal line marks the hypothetical top of the current viewport. In this case, this value will be the distance between the points marked as 3.
  • Using the two values we've calculated above, we can very quickly find out whether the sticky needs to be repositioned appropriately.

If you're confused, don't be. For example, let's look at some sample numbers:

  • The sticky's parent is present 10px from the top of the page.
  • The parent is 100px high.
  • The page has been scrolled 50px in one scenario and 150px in the other.

So based on this above information, you can deduce that

In scenario one - the sticky should be re-floated appropriately. Why? The page has been scrolled 10px from the top -- 10 comes from the page itself while the rest comes from the sticky's parent. Thus, the parent is visible in the main viewport.

In scenario two - the sticky can be left alone. Of the 150px, 10 comes from the page, 100 from the parent element and the rest is taken up by the rest of the page's element. This implies that the user has scrolled past the parent and we do not need to do anything.

If you're still fuzzy at this point, don't worry. I'll explain a bit more while walking through the source.


Deconstructing the Source

The source stripped of comments is only a smidgen over 30 lines long. As always, we'll walk through the code and explain what each line does.

Here's the source, for your reference.

$.fn.stickyfloat = function(options, lockBottom) {
				var $obj 				= this;
				var parentPaddingTop 	= parseInt($obj.parent().css('padding-top'));
				var startOffset 		= $obj.parent().offset().top;
				var opts 				= $.extend({ startOffset: startOffset, offsetY: parentPaddingTop, duration: 200, lockBottom:true }, options);
				
				$obj.css({ position: 'absolute' });
				
				if(opts.lockBottom){
					var bottomPos = $obj.parent().height() - $obj.height() + parentPaddingTop;
					if( bottomPos < 0 )
						bottomPos = 0;
				}
				
				$(window).scroll(function () { 
					$obj.stop();

					var pastStartOffset			= $(document).scrollTop() > opts.startOffset;	
					var objFartherThanTopPos	= $obj.offset().top > startOffset;	
					var objBiggerThanWindow 	= $obj.outerHeight() < $(window).height();
					
					if( (pastStartOffset || objFartherThanTopPos) && objBiggerThanWindow ){ 
						var newpos = ($(document).scrollTop() -startOffset + opts.offsetY );
						if ( newpos > bottomPos )
							newpos = bottomPos;
						if ( $(document).scrollTop() < opts.startOffset ) 
							newpos = parentPaddingTop;
			
						$obj.animate({ top: newpos }, opts.duration );
					}
				});
			};

Time to see what it actually does. I'm going to assume, you have a fairly basic grasp of JavaScript.

$.fn.stickyfloat = function(options, lockBottom)  {};

Step 1 - The generic wrapper for a jQuery plugin. As you probably know, options is an object containing assorted options to configure the behavior of the plugin. lockBottom, interestingly, specifies whether the functionality we want is turned on or not. We'll leave it on.

var $obj 				= this;

Step 2 - Keep a reference to the element passed. In this context, this points to the DOM element that matches to the selector you've passed in. For example, if you passed in #menu, this points to the element with that ID.

var parentPaddingTop 	= parseInt($obj.parent().css('padding-top'));

Step 3 - This is just to smooth out the effect is the parent element has a large padding. If so, this will include the padding in the calculation.

var startOffset 		= $obj.parent().offset().top;

Step 4 - We calculate the parent's position relative to the document using the offset jQuery method. We work through the DOM using the parent method. We $obj since we've already cached the sticky. Hit the jQuery API documentation if you're not familiar with these methods.

In this case, the distance from the top is sufficient so we'll acquire that value alone.

var opts 				= $.extend({ startOffset: startOffset, offsetY: parentPaddingTop, duration: 200, lockBottom:true }, options);

Step 5 - A pretty generic portion of the jQuery plugin development process. We essentially merge in the passed options along with some presets to get a final set of options that's used throughout the code. Keep in mind, that the passed parameters always take precedence over the defaults.

 $obj.css({ position: 'absolute' });

Step 6 - The effect in question will be created by manipulating the element's top CSS value so we'll just go ahead and set its position to absolute in case it hasn't been set that way already.

 if(opts.lockBottom){
					var bottomPos = $obj.parent().height() - $obj.height() + parentPaddingTop;
					if( bottomPos < 0 )
						bottomPos = 0;
				}

Step 7 - As noted above, the lockBottom option specifies whether the effect in question works or not. If enabled, we can start calculating. What we're calculating is the cutoff point beyond which we wouldn't need to reposition the sticky.

Naturally, you can go by just calculating the parent's height but the effect will be unrefined. You'll need to take into account the height of the sticky itself along any paddings on the parent itself.

$(window).scroll(function () { // Lots of code })

Step 8 - We hook our code, inside an anonymous function, to the windows' scroll event. Granted, this isn't the most efficient way to proceed but we'll ignore it for now.

$obj.stop();

Step 9 - First order of the say is to stop all running animations on the sticky element. The stop method takes care of this.

var pastStartOffset			= $(document).scrollTop() > opts.startOffset;	
var objFartherThanTopPos	= $obj.offset().top > startOffset;	
var objBiggerThanWindow 	= $obj.outerHeight() < $(window).height();

Step 10 - These three variables hold values which we'll make use of a bit later.

  • pastStartOffset checks whether we've scrolled past the top boundary of the parent element. Remember, we used the offset method to find out the space between the parent element and document. We obtain how far down you've scrolled using the scrollTop method. This is the distance between the top of the document and the top of the current viewport.
  • objFartherThanTopPos checks whether the sticky is in it's default position -- at the top of its parent. If we've scrolled beyond the top of the parent, we don't want it floating outside.
  • objBiggerThanWindow checks whether the total height of the sticky is bigger than the size of the window. If that's the case, there's no point in manipulating the sticky element.
if( (pastStartOffset || objFartherThanTopPos) && objBiggerThanWindow ){ // More code }

Step 11 - This is where the plugin calculates whether we'll need to manipulate the sticky element. What the above line does it:

  • Check whether user is scrolling exactly in the parent element's range. We check whether the user is below the parent's top boundary or alternatively the sticky is at the top.
  • As noted above, we proceed only if the sticky is smaller than the window size.

We proceed only if both of these conditions are satisfied.

var newpos = ($(document).scrollTop() -startOffset + opts.offsetY );

Step 12 - This line defines a variable, newpos, which specifies the position to which the sticky element has to be animated to. As you may noticed, the calculation is fairly basic if you keep in mind the image above. Find out the scrolled distance, add the parent's top padding to it and finally subtract the distance between the document and the parent-- the starting point. This gives you the distance, in pixels, between the top of the parent element to the point inside ,where the sticky should be positioned.

if ( newpos > bottomPos )
							newpos = bottomPos;

Step 13 - If we've scrolled beyond the bottom boundary of the parent element, no need to further manipulate things. Lock its position there.

if ( $(document).scrollTop() < opts.startOffset ) 
							newpos = parentPaddingTop;

Step 14 - If we've scrolled above the top boundary of the parent, keep it locked there so it doesn't move further up.

$obj.animate({ top: newpos }, opts.duration );

Step 15 - All done! We simply animate the sticky element passing in the required top value along with the duration of the effect using the animate jQuery method.


Usage

As you may have probably inferred at this point, usage is like so:

$('#menu').stickyfloat({ duration: 500 });>

Instead of explaining the sample mini-project, like last time, I've instead decided to merely build it and give you the code.

Here's the relevant parts of the demo, the rest is boilerplate:

The HTML

<div class="section">
<div id="menu" class="menu">Sticky menu</div>
<div class="content">I wanted to write something incredibly, unabashedly witty here. I failed. :(</div>
</div>

<div class="section">
	<div id="menu2" class="menu">Yep, I'll follow you everywhere as long as you're within my parent</div>
	<div class="content">You were expecting something clever here, didn't you? I know you did! Fess up!</div>
	</div>

The CSS

.section { 
	padding:10px; 
	width:900px; 
	margin:0 auto;
	background-color:#f1f1f1; 
	position:relative; 
}

.section .content { 
	height:800px; 
	background-color:#ddd; 
	margin-left:250px; 
	text-align:center; 
	color:#333; 
	font-size:16px; 
}

.section .menu { 
	position:absolute; 
	left:10px; 
	width:240px; 
	height:100px; 
	background: #06C; 
	text-align:center; 
	color:#fff; 
	font-size:14px; 
}

The JavaScript

$('#menu').stickyfloat({ duration: 400 });
$('#menu2').stickyfloat({ duration: 400 });

If you're going through the files as you read this article, it should be fairly self explanatory but you're more than welcome to hit me with questions if any part is unclear.


Wrapping Up

And we're done. We took a look at an incredibly useful plugin, walked through the source code and finally finished by creating a mini project with it.

Questions? Nice things to say? Criticisms? Hit the comments section and leave me a comment. Thank you so much for reading!