Stalker, a JQuery Plugin That Allows Elements to Follow Users on a Page

When we designed the new header for the Box Webapp, one of the goals was to make navigating the site easier and more intuitive, which we decided could be achieved by putting search front and center alongside access to the main areas of the application.  In order to have those controls readily at hand and for an enhanced user experience, we decided to have the header follow users as they scroll down a page. (You can see a very simple demo of how the plugin works here.)

Design decision: Building a jQuery Plugin

While similar design patterns exist all over the web, I wasn't able to find an existing solution that fit our needs.  So I decided to build my own jQuery plugin to manage this behavior, which gave us additional flexibility to use the "following while you scroll" behavior elsewhere in the application, and let us release the code for other developers to use in their own projects. I started with the basic use case: the header should follow a user down the page and then return to its original position at the top of the page when the user scrolls back up.  Starting from the jQuery Boilerplate, I added the core functionality to get the element to follow the user as they scroll: [js] function setPosition(edge) { me.stalking = true; var initial = {position: 'fixed'}, ending = $.extend({}, initial); initial[edge] = -(me.jElement.outerHeight()) + 'px'; ending[edge] = parseInt(me.options.offset) + 'px'; var handler = function() { // give the element custom style while stalking; by default, // force the element to have its original width and appear on top var stalker_css = $.extend({width: me._baseWidth + 'px', 'z-index': 999999}, me.options.stalkerStyle); me.jElement.css(stalker_css); me.jElement.css(ending); }; if (me.options.delay) { setTimeout(handler, me.options.delay); } else { handler(); } } var pageTop = $(document).scrollTop(); var viewportHeight = $(window).height(); var pageBottom = pageTop + viewportHeight; if (me.options.direction == 'down') { if ( < pageTop) { if (!me.stalking) { setPosition('top'); } } else { me.jElement.css(me._baseCSS); me.stalking = false; } } else { if ( + me.jElement.outerHeight() > pageBottom) { if (!me.stalking) { setPosition('bottom'); } } else { me.jElement.css(me._baseCSS); me.stalking = false; } } [/js]

Smooth Scrolling

To ensure that the scrolling experience remains smooth for the user, I used a placeholder with the same height as the original element to keep the page from shifting when the "following" element gets removed from the document flow. In the code, I created a simple placeholder element... [js]me.placeholder = $('<div></div>').css({height: this.jElement.outerHeight(), width: this._baseWidth});[/js] ...and inserted it when the element starts following: [js]me.jElement.before(me.placeholder).css(ending);[/js] Rather than using position: absolute and having to adjust the position of the element on each scroll event, I chose to use position: fixed and set it only once for efficiency.  Since the scroll event fires for each pixel the user scrolls up or down, it was imperative to do as little work as possible for each event.

Issue #1: Element Width

One of the first stumbling blocks I discovered while developing the basic behavior was that setting the position of the element wasn't enough.  Because our header has width: 100%, taking it out of the document flow with position: fixed actually caused it to shrink, since it had no container to expand into. Since it gave itself just the width it needed to contain its children, I had to manually give the element a width while it was following.

Issue #2: Restoring CSS

With that solved, the hard part was to put the element back when the user scrolled back up.  I started by just saving and restoring all the CSS properties that I planned to alter, as well as added functionality to apply some extra styles to the element while it was following. I used the extra style functionality to put a small drop shadow beneath the header, so it didn't look too flat and so that user has some indication that they're not really at the top of the page.

Issue #3: Page Resizing

The first problem I ran into with this design was when the page was resized, the header's width didn't change with the page, which looked pretty bad.  To solve this problem, I leveraged the placeholder element created earlier.  Since the placeholder was in the document flow in place of the original element, its width should be the right one to set for the "following" element.  To take advantage of this, I changed the placeholder from a generic <div> to a shallow clone of the "following" element, like so: [js] // we also need a placeholder to keep the document from reflowing // use a clone to keep styles (esp. those related to width) but remove // children to reduce id conflicts this.placeholder = this.jElement.clone(false).empty().css('height', this.jElement.outerHeight()); [/js]

Tweaking the Design

This also challenged my assumptions about how the element could and couldn't change while it was "following", and prompted another change: rather than saving and restoring CSS properties on the same element, I again used cloning to better preserve the original state of the "following" element so that when it was restored, the style changes made to it were not included. I made a clone of the "following" element in its original state: [js]this._jElementClone = this.jElement.clone(true, true);[/js] Then I did some swapping around to restore everything back to its original state: [js] /** * Restores an element to its original state after stalking * by refreshing it with its clone */ function restoreOriginalState() { // discard the stalker and all its weird inline styles me.jElement.remove(); me.placeholder.replaceWith(me._jElementClone); // make the old clone the element we're tracking me.jElement = me._jElementClone; // create a new clone, so the clone and element aren't the same thing me._jElementClone = me._jElementClone.clone(true, true); me.stalking = false; } [/js] The second obstacle I ran into was that there are a lot of controls in the header which depended on various event handlers to work correctly.  Cloning the header and all its descendants was keeping the event handlers associated with the header intact, but it meant that any handlers elsewhere that had cached references to elements in the header would no longer work, since it was now possible that the header could have been swapped out for a clone of itself.  Since the only element I really cared about cloning was the "following" element itself (in this case the header <div>) -- its descendants didn't matter for the purposes of keeping styles consistent.  So instead of a (more expensive) deep recursive clone of the element and everything inside it, I could just use a deep clone of the element and "scoop out" all its children when swapping in the clone: [js] function restoreOriginalState() { // discard the stalker and all its weird inline styles me.jElement.detach(); // rip the guts out of the original and dump them into the clone var contents = me.jElement.contents(); me._jElementClone.empty().append(contents); me.placeholder.replaceWith(me._jElementClone); // make the old clone the element we're tracking me.jElement = me._jElementClone; // create a new clone later, when we start stalking again me.stalking = false; } [/js] Note that the code also delays the creation of the clone until the very last moment before the element starts following again, so that any changes to it between the time the element stops following and starts following again will be saved.


Given how useful it has been for both our users and developers, we have open-sourced the plugin at -- check it out if you're interested! (You can see a very simple demo of how the plugin works here.) And if you end up using it for a project, or have any suggestions for how it might be improved, I'd love to hear about it!  You can send me a note at