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
- Capture a copy/cache of
lielements (for running the effect). - Limit the
ulto only show Nlielements.
Running the Effect
- Insert a new item at the top that is: opacity: 0 & height: 0.
- Fade the last item out.
- Increase first item’s height to real height.
- …at the same time, decrease the height of the last item.
- Once height changes have finished, remove the last item.
- 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);
}
You should follow me on Twitter here I tweet about jQuery amongst the usual tweet-splurges!
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
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>Simply Spy</title>
<style type="text/css" media="screen">
<!--
/* Style and images take for example purposes only from http://www.quicksnapper.com */
body { font: 1em "Lucida Grande",Lucida,Verdana,sans-serif; font-size: 62.5%; line-height: 1;}
input, textarea { font-family: Arial; font-size: 125%; padding: 7px; }
label { display: block; }
p { margin: 0; margin-bottom: 4px;}
h5 { margin: 0; font-weight: normal; }
a:link,
a:hover,
a:visited {
color: #fff;
}
#sidebar {
color: #AFB0B1;
background: #0D171A;
float:left;
margin:0 0 24px;
padding:15px 10px 10px;
width:300px;
}
#sidebar ul {
font-size:1.2em;
list-style-type:none;
margin:0;
padding:0;
position:relative;
}
.rating {
background-image:url(http://static.jqueryfordesigners.com/demo/images/simple-spy/info_bar_stars.png);
background-repeat:no-repeat;
height:12px;
text-indent:-900em;
font-size:1em;
margin:0 0 9px;
}
.none {
background-position: 82px 0px;
}
.four {
background-position: 82px -48px;
}
.five {
background-position: 82px -60px;
}
.tags {
color: #fff;
margin: 0.5em;
}
.tags a,
.tags span {
background-color: #333839;
font-size: 0.8em;
padding: 0.1em 0.8em 0.2em;
}
.tags a:link,
.tags a:visited {
color: #fff;
text-decoration: none;
}
.tags a:hover,
.tags a:active {
background-color: #3e4448;
color: #fff;
text-decoration: none;
}
#sidebar li {
height: 90px;
overflow: hidden;
}
#sidebar li h5 {
color:#A5A9AB;
font-size:1em;
margin-bottom:0.5em;
}
#sidebar li h5 a {
color:#A5A9AB;
text-decoration:none;
}
#sidebar li img {
float:left;
margin-right:8px;
}
#sidebar li .info {
color:#3E4548;
font-size:1em;
}
#sidebar .info a,
#sidebar .info a:visited {
color:#3E4548;
text-decoration: none;
}
#sidebar .spyWrapper {
height: 100%;
overflow: hidden;
position: relative;
}
#sidebar {
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
}
.tags span,
.tags a {
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
}
a img {
border: 0;
}
-->
</style>
<script src="jquery-1.2.6.js" type="text/javascript"></script>
<script type="text/javascript" charset="utf-8">
$(function () {
$('ul.spy').simpleSpy();
});
(function ($) {
$.fn.simpleSpy = function (limit, interval) {
limit = limit || 4;
interval = interval || 4000;
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),
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;
$list.wrap('<div class="spyWrapper" />').parent().css({ height : height * limit });
$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;
}
setTimeout(spy, interval)
}
spy();
});
};
})(jQuery);
</script>
</head>
<body>
<h1>Simple Spy</h1>
<div id="sidebar">
<ul class="spy">
<li>
<a href="#" title="View round"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/1.png" title="" /></a>
<h5><a href="#" title="View round">round</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit neue's userpage.">neue</a></p>
<p class='rating none'>Not Rated</p>
<p class="tags"></p>
</li>
<li>
<a href="#" title="View reflet"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/2.png" title="" /></a>
<h5><a href="#" title="View reflet">reflet</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit neue's userpage.">neue</a></p>
<p class='rating none'>Not Rated</p>
<p class="tags"><a href="#" title='Find more images tagged Tactile'>Tactile</a></p>
</li>
<li>
<a href="#" title="View Kate Moross Little Big Planet"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/3.png" title="" /></a>
<h5><a href="#" title="View Kate Moross Little Big Planet">Kate Moross Little Big Planet</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit neue's userpage.">neue</a></p>
<p class='rating four'>Four Stars</p>
<p class="tags"><a href="#" title='Find more images tagged Kate Moross'>Kate Moross</a></p>
</li>
<li>
<a href="#" title="View Untitled"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/4.png" title="" /></a>
<h5><a href="#" title="View Untitled">Untitled</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit mike1052's userpage.">mike1052</a></p>
<p class='rating none'>Not Rated</p>
<p class="tags"></p>
</li>
<li>
<a href="#" title="View My Tutorial's Library"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/5.png" title="" /></a>
<h5><a href="#" title="View My Tutorial's Library">My Tutorial's Library</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit FrancescoOnAir's userpage.">FrancescoOnAir</a></p>
<p class='rating five'>Five Stars</p>
<p class="tags"></p>
</li>
<li>
<a href="#" title="View Sandy — your free personal email assistant - Log in"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/6.png" title="" /></a>
<h5><a href="#" title="View Sandy — your free personal email assistant - Log in">Sandy — your free</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit John Doe's userpage.">John Doe</a></p>
<p class='rating none'>Not Rated</p>
<p class="tags"><a href="#" title='Find more images tagged Blue'>Blue</a> <a href="#" title='Find more images tagged I Want Sandy'>I Want Sandy</a></p>
</li>
<li>
<a href="#" title="View Sandy — your free personal email assistant - Log in"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/7.png" title="" /></a>
<h5><a href="#" title="View Sandy — your free personal email assistant - Log in">Sandy — your free</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit John Doe's userpage.">John Doe</a></p>
<p class='rating none'>Not Rated</p>
<p class="tags"><a href="#" title='Find more images tagged Blue'>Blue</a> <a href="#" title='Find more images tagged I Want Sandy'>I Want Sandy</a></p>
</li>
<li>
<a href="#" title="View Sandy — your free personal email assistant"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/8.png" title="" /></a>
<h5><a href="#" title="View Sandy — your free personal email assistant">Sandy — your free</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit John Doe's userpage.">John Doe</a></p>
<p class='rating none'>Not Rated</p>
<p class="tags"><a href="#" title='Find more images tagged Blue'>Blue</a> <a href="#" title='Find more images tagged Homepage'>Homepage</a></p>
</li>
<li>
<a href="#" title="View Values of n Blog"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/9.png" title="" /></a>
<h5><a href="#" title="View Values of n Blog">Values of n Blog</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit John Doe's userpage.">John Doe</a></p>
<p class='rating none'>Not Rated</p>
<p class="tags"><a href="#" title='Find more images tagged Brown'>Brown</a> <a href="#" title='Find more images tagged Blogs'>Blogs</a></p>
</li>
<li>
<a href="#" title="View why values of n"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/10.png" title="" /></a>
<h5><a href="#" title="View why values of n">why values of n</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit John Doe's userpage.">John Doe</a></p>
<p class='rating none'>Not Rated</p>
<p class="tags"><a href="#" title='Find more images tagged Brown'>Brown</a> <a href="#" title='Find more images tagged Values of N'>Values of N</a></p>
</li>
<li>
<a href="#" title="View values of n"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/11.png" title="" /></a>
<h5><a href="#" title="View values of n">values of n</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit John Doe's userpage.">John Doe</a></p>
<p class='rating none'>Not Rated</p>
<p class="tags"><a href="#" title='Find more images tagged Brown'>Brown</a> <a href="#" title='Find more images tagged Homepage'>Homepage</a></p>
</li>
<li>
<a href="#" title="View stikkit privacy policy"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/12.png" title="" /></a>
<h5><a href="#" title="View stikkit privacy policy">stikkit privacy policy</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit John Doe's userpage.">John Doe</a></p>
<p class='rating none'>Not Rated</p>
<p class="tags"><a href="#" title='Find more images tagged Yellow'>Yellow</a> <a href="#" title='Find more images tagged Blue'>Blue</a></p>
</li>
<li>
<a href="#" title="View stikkit: the stikkit api"><img width="70" height="70" src="http://static.jqueryfordesigners.com/demo/images/simple-spy/13.png" title="" /></a>
<h5><a href="#" title="View stikkit: the stikkit api">stikkit: the stikkit api</a></h5>
<p class="info">Nov 29th 2008 by <a href="#" title="Visit John Doe's userpage.">John Doe</a></p>
<p class='rating none'>Not Rated</p>
<p class="tags"><a href="#" title='Find more images tagged Yellow'>Yellow</a> <a href="#" title='Find more images tagged Blue'>Blue</a></p>
</li>
</ul>
</div>
</body>
</html>

Play QuickTime version
Play Flash version

Mark Henderson On 2nd December 2008 at 16:12
The site has been quiet for a while… This looks really good and useful!! Thanks for sharing..
Mark Henderson On 2nd December 2008 at 16:12
All the sites I look after are financial I would love to use this effect with a news feed!
Remy On 2nd December 2008 at 16:12
@Mark - I’ll be doing a follow up post on how to link this kind of effect to an Ajax feed.
Re: being quiet - guilty as charged. Been busy with work, but I’ve got 3 more articles lined up for the next few weeks.
Paul Grenier On 2nd December 2008 at 17:12
I’m new to jQuery and have only been teaching myself JavaScript for the past few months, so the part I found most interesting is following your “development style.”
Thanks.
digital On 2nd December 2008 at 17:12
Great to see a new post on here, concise, easy to understand and as always an excellent result.
Thanks!
Ronald On 2nd December 2008 at 17:12
Great job! remember to post with the ajax feed :o)
Ronald On 2nd December 2008 at 18:12
Great job! remember the post with the ajax feed :o)
Zach LeBar On 2nd December 2008 at 18:12
I’m a huge fan of jQuery for Designers. I don’t really mind the time that goes by, I know ur busy, we’re all busy. And they are always some of the most in-depth tuts out there for jQuery, and javascript in general. PLEASE keep ‘em coming!
-Zach LeBar
zero0x On 2nd December 2008 at 18:12
hey..
wouldn’t it be better to cache $(this).clone() instead of $(this).html() … ??
I mean to heave the whole
<li>object, not only the contents..Remy On 2nd December 2008 at 19:12
@zero0X - I wrote the first version using .clone() but there was an odd problem in IE, in that once it had completed the loop, putting the first one back on the list appeared to be completely blank, even though the DOM was actually there. Very strange. I think the .clone() method was also better on resources annoyingly.
Daniel On 2nd December 2008 at 20:12
Really cool stuff Remy. I really appreciate the work you’re putting into this site. Following your workflow and your in depth tutorial/demos is very very educational. Your demos are always to the point and proper “real life” examples.
I do experience a small glitch in the animation in IE7 on XP. Anyone else experiencing this?
I also wonder if there is a nice way to get rid of the IE cleartype issue when fading text that doesn’t involve too much code?
Mat On 2nd December 2008 at 21:12
Very nice. Thanks Remy.
When the ticker initially rolls in to action, it drops off the 4th item (untitled) and pulls in the 6th item (Sandy - your free), thereby skipping the 5th item (My Tutorial’s Library). Wouldn’t it make more sense to pull in the 5th item first? I realise this can achieved by removing the ‘+1′ from the ‘currentItem’ variable but maybe there was a reason for this?
Daniel On 2nd December 2008 at 21:12
—- EDIT TO PREVIOUS —-
Just to make it clear, the glitch I’m experiencing is the LI’s stopping for a fraction of a second when animating down and then popping into place. It’s not the problem you discuss in the screencast where the whole div would jump. I see it in both the example you posted and in the one I made myself. Unfortunately I can’t compare it to the orignal since it doesn’t work at all in IE.
—- END EDIT —
Remy On 2nd December 2008 at 22:12
@Mat - you’re right, there shouldn’t be a ‘+1′ - it was a left over from another way I was trying. I’m going to update the code to remove the +1.
@Daniel - I know what you’re talking about - I did spot that in IE. If I’ve got time, I’ll take a look at it. I suspect it’s inheriting the browser’s padding(?). Perhaps including a reset stylesheet might help, or at least try:
Daniel On 3rd December 2008 at 00:12
Have quickly tried some different resets and css settings to zero including h5,img,a,p,li,ul. Also tried removing the p tags completely ant it still jumps. Tried setting margin and padding to 0 at the same time as setting height and opacity to 0 for the $insert variable and no luck. Tried the Meyer reset as well.
Only had time for a quick look now, getting late here. Will get back on it tomorrow :]
Thanks again for great inspiration!
caruso_g On 3rd December 2008 at 01:12
Remy, don’t worry, we are all busy but here the time spent waiting for your great tuts is widely paid back! Just a question, is it possible to stop it on mouse hover?
Felix On 3rd December 2008 at 04:12
Super sleek! I always wanted to do the spy stuff. Thanks.
Steven Black On 3rd December 2008 at 06:12
Thanks Rem!
Each time I watch one of your videos I learn something new about FireBug.
Therefore, a suggestion for a future screencast: “Working with FireBug Part I”
k3k On 3rd December 2008 at 10:12
A search engine friendly and useful solution. Thanks K3k
Baga On 4th December 2008 at 08:12
Excellent stuff, thanks a lot. Anyone knows how this effect can be implemented using the prototype library instead of jquery?
Erik Wulf-steen On 4th December 2008 at 21:12
Thanx man, this is mint!!! Great to see you in action.
Urz
Erik
cody On 4th December 2008 at 22:12
Very nice!
One thought…
I believe you can just do this…
instead of this
easyman On 10th December 2008 at 00:12
+1 for mouse over pause, and possibly a stop/start option too!
phil On 15th December 2008 at 10:12
Hi guys,
A fix for the jump in IE7 - simply add a float to the main list element.
I wanted to add some padding as well, so I simply made an inner div which keeps everything nice and smooth.
Example:
Hope it helps everyone!
Tai On 19th December 2008 at 06:12
I arranged the movement from right to left.
Changed the height to width on Javascript, and fixed the
<li>float to right.