Search

Simple jQuery Spy Effect

Posted on 2nd December 2008 — A few years ago Digg released a very cool little visualisation tool they dubbed the Digg Spy (it’s since been upgraded to the Big Spy). Recently Realmac Software released the site QuickSnapper to accompany LittleSnapper.

It’s the QuickSnapper site (the left hand side) that makes use of the similar spy technique that I’ll explain how to produce.

Watch

Watch the jQuery spy screencast (alternative flash version)

QuickTime version is approximately 60Mb, flash version is streaming.

View the demo and source code used in the jQuery spy screencast

Simple Spy

The great thing about Realmac’s QuickSnapper site is that if JavaScript is turned off, the list of snaps is visible by default. So we’ll follow suit.

It’s also worth noting that their version only keep pulling in new items until it hits the end. I’ll show you how you can keep the list looping, and in a follow up tutorial I’ll show you how to hook this in to an Ajax hit that doesn’t hammer your server and keeps the effect nice and smooth.

Development Tasks

I’ve broken down what needs to happen to be able to recreate this effect:

Setup

  1. Capture a copy/cache of li elements (for running the effect).
  2. Limit the ul to only show N li elements.

Running the Effect

  1. Insert a new item at the top that is: opacity: 0 & height: 0.
  2. Fade the last item out.
  3. Increase first item’s height to real height.
  4. …at the same time, decrease the height of the last item.
  5. Once height changes have finished, remove the last item.
  6. Repeat.

HTML

The HTML is very simple for the effect - since the non-JS version of the page the HTML appears the same, except with a longer list.

As such, rather than the complete listing, which you can see in the live demo, we’re just using a simple list element:

<ul class="spy">
    <li>
      <!-- contents of list item -->
    </li>
</ul>

jQuery

For this example of code, we’re creating a reusable plugin. So I’ve wrapped our code in the follow pattern:

(function ($) {
  
// our plugin goes here
  
})(jQuery)

This allows me to reference the $ variable knowing that it won’t conflict with other libraries such as Prototype - because we’ve passed the jQuery variable in to the function in the following line:

})(jQuery)

Version 1: simultaneously height animate

This is the first version of the plugin. It resizes the first and last item simultaneously:

(function ($) {    
$.fn.simpleSpy = function (limit, interval) {
  // set some defaults
  limit = limit || 4;
  interval = interval || 4000;
  
  return this.each(function () {
    // 1. setup
      // capture a cache of all the list items
    var $list = $(this),
      items = [], // uninitialised
      currentItem = limit,
      total = 0, // initialise later on
      height = $list.find('> li:first').height();
          
    // capture the cache
    $list.find('> li').each(function () {
      items.push('<li>' + $(this).html() + '</li>');
    });
    
    total = items.length;
    
    // chomp the list down to limit li elements    
    $list.find('> li').filter(':gt(' + (limit - 1) + ')').remove();

    // 2. effect        
    function spy() {
      // insert a new item with opacity and height of zero
      var $insert = $(items[currentItem]).css({
        height : 0,
        opacity : 0,
        display : 'none'
      }).prependTo($list);
                    
      // fade the LAST item out
      $list.find('> li:last').animate({ opacity : 0}, 1000, function () {
        // increase the height of the NEW first item
        $insert.animate({ height : height }, 1000).animate({ opacity : 1 }, 1000);

        // AND at the same time - decrease the height of the LAST item
        $(this).animate({ height : 0 }, 1000, function () {
            // finally fade the first item in (and we can remove the last)
            $(this).remove();
        });
      });
        
      currentItem++;
      if (currentItem >= total) {
        currentItem = 0;
      }
        
      // trigger the effect again in 4 seconds
      setTimeout(spy, interval);
    }
    
    spy();
  });
};    
})(jQuery);

Version 2: fixed height

The second version has the following changes, and allows us to remove one of the animations, reducing the work the browser has to do.

We do this by created a fixed height wrapper around the ul.spy, and it works because the styling is on an outer div.

We change:

// chomp the list down to limit li elements
$list.find('> li').filter(':gt(' + (limit - 1) + ')').remove();

To add a line before that wraps our spy:

$list.wrap('<div class="spyWrapper" />').parent().css({ height : height * limit });

// chomp the list down to limit li elements
$list.find('> li').filter(':gt(' + (limit - 1) + ')').remove();

Then we need to comment out the animate height to zero.

We change:

// AND at the same time - decrease the height of the LAST item
$(this).animate({ height : 0 }, 1000, function () {
    // finally fade the first item in (and we can remove the last)
    $(this).remove();
});

To only remove the element:

// finally fade the first item in (and we can remove the last)
$(this).remove();

Version 3: loop once

Though I didn’t cover this in the screencast, it’s simple to change the spy to run once, by changing the following:

if (currentItem >= total) {
  currentItem = 0;
}
  
// trigger the effect again in 4 seconds
setTimeout(spy, interval)

To, if we’ve hit the limit, then don’t set a new timeout:

if (currentItem >= total) {
  // let the spy finish
} else {
  // trigger the effect again in 4 seconds
  setTimeout(spy, interval);
}

Related screencasts

Demo

If you find this demo doesn't work as expected, it's possibly due to the demo running from within an iframe. Try running the demo in it's own window.

Source Code

Comments

  1. Dave Redfern On 21st December 2008 at 17:12

    Hello Everyone,

    I love this effect and currently implementing it into one of my websites. How do i get it so the effect pauses while someones mouse is over a story?

    Thanks

    Dave/

  2. Jewelry On 1st January 2009 at 12:01

    Thanx man, this is mint!!! Great to see you in action.

    Urz

  3. Mustafa Kahraman On 2nd January 2009 at 02:01

    Hi all, Very nice video ! Thanks for teaching pls keep going :) +1 pause start/stop methods

  4. Jay On 9th January 2009 at 15:01

    Fantastic video! I particularly appreciated that we got to see your thought process in action! Polished ‘casts which only show me the “green path” aren’t nearly as useful as learning some of the side tricks that you showed!

    Bravo and thanks again!

  5. BinaryKitten On 12th January 2009 at 18:01

    just a note … shouldn't items.push('<li>' + $(this).html() + '</li>'); be items.push('<li>' + $(this).html() + '');

    as noted here: http://www.htmlhelp.com/tools/validator/problems.html

  6. Remy On 12th January 2009 at 18:01

    @BinaryKitten - no - definitely not - because you’re not closing the opening li element.

    Keep in mind we’re building up an array of strings that look like this:

    <li><a href="..."><img src="..." /></a></li>

  7. nadia On 16th January 2009 at 03:01

    remy , this’s very nice script but when I loop continually, browser (firefox) alway using 90-100% cpu resources

    • ie slow down when i use this script
    • safari same ff … alway 90-100% cpu

    and i see in firebug when looping why image http alway request, all images have load already but when looping, it’s loading again… i think this script just fade in - fade out the <li></li> that already load completely

  8. nadia On 16th January 2009 at 04:01

    hi, how to modify code if i would like to have

    • loop 2 times and stop
    • waiting [x] seconds and start looing(2 times) again

    another thing now i’m using Version 3: loop once

    i have 8 posts

    1 2 3 4 5 6 7 8

    it’s start with post 1-4 1 2 3 4

    and put post 5 to top, put post 6 to top….

    and stop with

    8 7 6 5

    i want it stop on same start post (because latest posts are 1-4)

    1 2 3 4

    i think it’s run like a circle… first show 1 2 3 4 and put 8 to top, put 7 to top put 6 to top….

  9. Jan Komzak (comz) On 27th January 2009 at 16:01

    Hello, I’ve been inspired by your screencast, so I take some time and re-made this code for MooTools. You could see that at http://blog.comz.cz/2009/01/27/mootools-plugin-fx-spyeffect . The post is written in czech, so you could use Google Translator. I want to spend more time to add some features, such as stopping on mouseenter and other, so if you want to consult that, feel free to contact me :) Jan Komzak @ comz, s.r.o.

  10. H.P. On 2nd February 2009 at 18:02

    What a nice effect. We will try to take it for a special product presentation in our shopsoftware.

  11. senthil On 12th February 2009 at 06:02

    I need to stop effect on mouseover.. Is it possible to do tat?? if so may i know how can i do it?

  12. esperi On 24th February 2009 at 15:02

    great job!

    for mouse over pause, and possibly a stop/start option too!

  13. esperi On 24th February 2009 at 15:02

    //—-> for mouse over pause

    $(function () { $(’ul.spy’).simpleSpy().bind(’mouseenter’, function () { $(this).trigger(’stop’); }).bind(’mouseleave’, function () { $(this).trigger(’start’); }); });

    (function ($) {

    $.fn.simpleSpy = function (limit, interval) { limit = limit || 2; interval = interval || 4000;

    function getSpyItem($source) {
        var $items = $source.find('> li');
    
        if ($items.length == 1) {
            // do an hit to get some more
            $source.load('block/data.php');
        } else if ($items.length == 0) {
            return false;
        }
    
        // grab the first item, and remove it from the $source
        return $items.filter(':first').remove();
    }
    
    return this.each(function () {
        // 1. setup
            // capture a cache of all the list items
            // chomp the list down to limit li elements
        var $list = $(this),
            running = true,
            height = $list.find('> li:first').height();
    
        // TODO create the $source element....
        var $source = $('<ul />').hide().appendTo('body');
    
        $list.wrap('<div class="spyWrapper" />').parent().css({ height : height * limit });
    
        $list.find('> li').filter(':gt(' + (limit - 1) + ')').appendTo($source);
    
        $list.bind('stop', function () {
            running = false;
        }).bind('start', function () {
            running = true;
        });
    
        // 2. effect
        function spy() {
            if (running) {
                var $item = getSpyItem($source);
    
                if ($item != false) {
                    // insert a new item with opacity and height of zero
                    var $insert = $item.css({
                        height : 0,
                        opacity : 0,
                        display : 'none'
                    }).prependTo($list);
    
                    // fade the LAST item out
                    $list.find('> li:last').animate({ opacity : 0}, 1000, function () {
                        // increase the height of the NEW first item
                        $insert.animate({ height : height }, 1000).animate({ opacity : 1 }, 1000);
    
                        // AND at the same time - decrease the height of the LAST item
                        // $(this).animate({ height : 0 }, 1000, function () {
                            // finally fade the first item in (and we can remove the last)
                            $(this).remove();
                        // });
                    });             
                }                
            }
    
            setTimeout(spy, interval);
        }
    
        spy();
    });
    

    };

    })(jQuery);

  14. esperi On 24th February 2009 at 15:02

    for mouse over pause:

    $(function () { $('ul.spy').simpleSpy().bind('mouseenter', function () { $(this).trigger('stop'); }).bind('mouseleave', function () { $(this).trigger('start'); }); });

    (function ($) {

    $.fn.simpleSpy = function (limit, interval) { limit = limit || 2; interval = interval || 4000;

    function getSpyItem($source) {
        var $items = $source.find('> li');
    
        if ($items.length == 1) {
            // do an hit to get some more
            $source.load('block/data.php');
        } else if ($items.length == 0) {
            return false;
        }
    
        // grab the first item, and remove it from the $source
        return $items.filter(':first').remove();
    }
    
    return this.each(function () {
        // 1. setup
            // capture a cache of all the list items
            // chomp the list down to limit li elements
        var $list = $(this),
            running = true,
            height = $list.find('> li:first').height();
    
        // TODO create the $source element....
        var $source = $('<ul />').hide().appendTo('body');
    
        $list.wrap('<div class="spyWrapper" />').parent().css({ height : height * limit });
    
        $list.find('> li').filter(':gt(' + (limit - 1) + ')').appendTo($source);
    
        $list.bind('stop', function () {
            running = false;
        }).bind('start', function () {
            running = true;
        });
    
        // 2. effect
        function spy() {
            if (running) {
                var $item = getSpyItem($source);
    
                if ($item != false) {
                    // insert a new item with opacity and height of zero
                    var $insert = $item.css({
                        height : 0,
                        opacity : 0,
                        display : 'none'
                    }).prependTo($list);
    
                    // fade the LAST item out
                    $list.find('> li:last').animate({ opacity : 0}, 1000, function () {
                        // increase the height of the NEW first item
                        $insert.animate({ height : height }, 1000).animate({ opacity : 1 }, 1000);
    
                        // AND at the same time - decrease the height of the LAST item
                        // $(this).animate({ height : 0 }, 1000, function () {
                            // finally fade the first item in (and we can remove the last)
                            $(this).remove();
                        // });
                    });             
                }                
            }
    
            setTimeout(spy, interval);
        }
    
        spy();
    });
    

    };

    })(jQuery);

  15. mrtech On 11th March 2009 at 19:03

    Upon Rey Bango’s recommendation I implemented this on my site, but had a requirement to keep the <li> formatting here are two ways I was able to do this, first the hardway, then the easy way:

    
            // capture the cache and carry over class, you can do style too
            $list.find('> li').each(function () {
                items.push('' + $(this).html() + '');
            });
    

    then the easy way:

    
            // capture the cache and carry the whole node
            $list.find('> li').each(function () {
                items.push($(this));
            });
    

  16. Rahul On 6th April 2009 at 10:04

    Man, this is a killer jQuery effect. Just superb, out of the world.

  17. David On 30th April 2009 at 23:04

    Hey all, i was wondering if anyone could let me know how to fade it as it drops out?

    Instead of using 4 for limit = limit || 4;

    I’m using: limit = limit || 2;

    It fades in but the other just drops off without fading.

    Any help would be great :o)

    David

  18. Sensei On 7th May 2009 at 17:05

    I love your tutorials, thats why i always check every new tutorial from your site using netnewswire.

    Can I ask you a question? Would you be so kind to let me know how to add a previous or forward buttom. I added it many “li”s so it takes a while to show the same “li” again in case you were checking them all. Please please ^_^ I will donate you for a couple beers and cigarettes -still a student sumimasen, i wish to give more (m_m) - but please mister how to add a prev/next buttom à la quicksnapper.com

    お願いします (^_^ ) = Onegai Shimasu = Please

    Thanks in advance and pls let me know where to donate.

    Greetings from Japan

  19. LuK On 29th July 2009 at 22:07

    @esperi

    I tried to implement your mouseover stop and mouseout start again method but I couldn’t get it to work, what I don’t understand is, how you can trigger something called stop and start, I thought the trigger function is only for events and as far as I know there are no events start and stop, there is a .stop() function…

  20. LuK On 9th August 2009 at 19:08

    =)…found the solution some inches away…just here: http://jqueryfordesigners.com/video.php?f=ajax-spy.flv –> this is the more actual Version with AJAX support and at around 14min in the video there are the exact steps showed to make the thing stop on mouseover…thx again for your nice tutorials!!!

  21. kamal On 29th August 2009 at 17:08

    Thanks for this ^power effect, so power.

  22. Free.Styler_1@yahoo.com On 6th September 2009 at 00:09

    Very Cool Effect… But please guys, How can i put 2 buttons in the top and in the bottom of the list to move the elements up and down manually ???

  23. Ben On 29th September 2009 at 22:09

    This doesn’t seem to work on IE7. Which is strange, because it does work in ie8 / ie6… any help?

  24. Lance On 30th September 2009 at 19:09

    Just curious if anyone could help show me how to make the images go from left to right (or right to left) .

    thanks! L

  25. Ben On 3rd October 2009 at 19:10

    How can I make it so this script won’t do anything in IE7? since IE7 bugs like a mofo.

Comments are now closed.