Watch
Watch jQuery look: Tim Van Damme screencast (Alternative flash version)
QuickTime version is approximately 60Mb, flash version is streaming.
View the demo used in the screencast
Deconstructing the effects
The first thing we need to do is look at the site in a WebKit browser (Safari’s my poison) and in a non-WebKit (Firefox will do), then view the page with JavaScript turned off, then look at the URLs and see the effect of landing on the page with the hash in the URL.
Hover shift
The roll over effect on the li elements (Twitter, Delicious, etc) shift softly to the left in WebKit. In Firefox they jump.
If you look at the stylesheet you’ll see this is being achieved with CSS transitions:
#networks li a img {
float: left;
margin: 0 10px 0 15px;
-webkit-transition: margin-left 0.25s linear;
}
#networks li a:hover img {
margin-left: 10px;
}
I’m a big fan of Progressive Enrichment, but there’s still an argument for matching functionality across browsers (government funded projects for instance) - so let’s look at doing this in jQuery so that all browser have the same functionality.
There’s actually two aspects we need to recreate using jQuery:
- Animated margin-left
- Background rgba crossfade (to create a darkened effect)
Animated margin-left
This is relatively simple. Tim’s site is floating the icon to the left, so the text rests against it. So when you hover over the link, the margin-left is reduced to 5px from 10px.
$('a').hover(function () {
$('img', this).stop().animate({
marginLeft : 5
}, 250);
}, function () {
$('img', this).stop().animate({
marginLeft : 10
}, 250);
});
This code triggers when the user hovers over the anchor element and animates the margin-left over 250ms.
We include the .stop() function to prevent the animation from continuously firing (give it a try without the .stop to see the effect).
However! We want the effect to still work without JavaScript enabled, so the following CSS is in place:
a:hover img {
margin-left: 5px;
}
The problem now is that when we roll the mouse over the anchor element for the first time it jumps to the left. Subsequent times it doesn’t jump. This is because an inline style is being applied.
To fix this we can apply the default margin-left manually (this is a nasty work around, but does the trick for this effect):
$('a').hover(fn1, fn2).css('marginLeft', 10);
Where fn1 and fn2 are the functions defined earlier.
Recreating rgba
Next up is recreating the rgba CSS3 effect. rgba is background colour with alpha transparency as the fourth parameter, e.g.:
background: rgba(164, 173, 183, .15);
I will be the first one to point out that this is a rather extreme approach to achieving this effect, and I would in all cases fight to use plain CSS to achieve this effect.
However, in the name of education - let’s see how we can create this effect.
We will need a block div that sits within the anchor element, using position: absolute and filling the area being the link. We can’t simply change the opacity of the background as this will change the opacity of the entire DOM fragment.
When the user hovers over the anchor element, we will fade the opacity up and down between 0 and 0.15.
// insert the rgba fader element
$('a').each(function () {
$('<div class="fader" />').prependTo(this);
})... // chain the hover functions
We style the element as:
.fader {
opacity: 0;
-moz-opacity: 0;
filter:alpha(opacity=0);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 60px; /* because IE6 isn't listen to 100% */
background-color: #A4ADB7;
}
I also noticed a problem in IE6, similar to the jumping margin - where the background become solid the first time and then fades correctly. So I’ve changed the prepend to set the opacity at the same time.
$('<div class="fader" />').css('opacity', 0).prependTo(this);
Now we include the fake rgba element fading in the hover function:
.hover(function () {
$('img', this).stop().animate({
marginLeft : 5
}, 250);
$('.fader', this).stop().animate({
opacity : 0.15
});
}, function () {
$('img', this).stop().animate({
marginLeft : 10
}, 250);
$('.fader', this).stop().animate({
opacity : 0
});
});
This completes the hover effect.
Accordion bounce
Accordion bounce is just a name I’ve made up. The effect is an accordion, but since it’s dropping down and up again, I’m saying it bounces!
If you look carefully when tabbing between the panels not only does the panel slide up and down, but it also fades out as it’s sliding up.
Also if you play with Tim’s version and click many tabs the slide effect goes a little haywire - which is where the .stop() method is required. However, there’s also a small jump that happens when switching from the ‘contact’ to the ‘about’. This is because the slideDown method uses the last height that it was before a call to slideUp. In this demonstration, I’m going to try to prevent both of the subtle issues.
Markup
The markup for my example is your basic ul + div combo that you’ll see in any traditional tab markup pattern.
<div>
<ul>...</ul>
<div class="panels">
<div id="a">...</div>
<div id="b">...</div>
<div id="c">...</div>
</div>
</div>
jQuery
I’ve written the code so that it’s the beginnings of a plugin and can be dropped in to any web site with a small tweak in the code here and here.
Our animation requires the following steps to work:
- Clicking on any navigation link will trigger the animation
- If the link is already selected, we’ll ignore the animation
- Remove the selected class from the navigation and add it to the currently clicked link
- If the browser supports opacity, fade the panels out†
- Slide the panel container up
- Once the slide up is finished, show the select panel, and slide down
- Finally, when the page has loaded, if a hash is on the URL, show that panel or show the first panel
† This is because IE6’s cleartype issue causes the text to look rather nasty when animating.
Setup
There’s a number of items we need to capture at the beginning of our code to create the effect.
// as we're creating the beginnings of a plugin - let's start with .each
$('.navigation').each(function () {
var $links = $(this).find('a'), // collect the links
// run through the link's hash and create #network,#about,#contact
panelIds = $links.map(function() { return this.hash; }).get().join(","),
// now grab a jQuery instance of these elements, i.e. the 3 panels
$panels = $(panelIds),
// look for the wrapping element and cache it
$panelwrapper = $panels.filter(':first').parent(),
// animation delay, so everything runs together
delay = 500,
/* animation height offset - required to avoid the jumping
we could/should calculate this by adding following from $panelwrapper:
o margin-top
o margin-bottom
o padding-top
o padding-bottom
*/
heightOffset = 40;
// hide all the panels initially
$panels.hide();
// TODO add click handler code
});
The line:
panelIds = $links.map(function() { return this.hash; }).get().join(","),
Is using the map function to loop through all the navigation links, and return a jQuery object containing only the ids of the anchors. By calling .get().join(',') it returns a CSV of ids, which we can then use as a selector. This is the equivalent to:
$panels = $('#networks, #about, #contact');
Click handler
The click handler code is where all the animation happens, and we have to tick our list off from above.
$links.click(function () {
// cache a copy of the DOM element, and jQueryifed element
var link = this,
$link = $(this);
// (2) If the link is already selected, we'll ignore the animation
if ($link.is('.selected')) {
return false;
}
// (3) Remove the selected class from the navigation and add it to
// the currently clicked link
$links.removeClass('selected');
$link.addClass('selected');
// a nice touch from Tim's version, change the document title,
// this could use link.title if we wanted to show something different
document.title = 'jQuery look: Tim Van Damme - ' + $link.text();
// (4) If the browser supports opacity, fade the panels out
if ($.support.opacity) {
$panels.stop().animate({opacity: 0 }, delay);
}
// Slide the panel container up, we're using animate to avoid jumping issues
$panelwrapper.stop().animate({
height: 0
}, delay, function () {
var height = $panels.hide() // hide all panels again
.filter(link.hash) // narrow down to this, e.g. #contact
.css('opacity', 1) // reset the opacity (from step 4)
.show() // reset the display prop (also from step 4)
.height() + heightOffset; // return and store the height to animate to
// (6) Once the slide up is finished, show the select panel, and slide down
$panelwrapper.animate({
height: height
}, delay);
});
});
Auto-selection
The final task to support hashes on the URL (step 7 above). So we add the following code below the click handler:
// if there is a hash on the url, filter the links by the hash prop
var toshow = window.location.hash ? '[hash=' + window.location.hash + ']' : ':first';
// now filter the links by the selector, and trigger a click.
$links.filter(toshow).click();
You can view the final working version from this tutorial and the screencast
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 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>jQuery look: Tim Van Damme</title>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<style type="text/css" media="screen">
body {
font-family: "Lucida Grande",Verdana,sans-serif;
font-size: 11px;
line-height: 16px;
padding: 10px;
}
h2 {
margin-top: 0;
}
#wrapper {
width: 470px;
margin: 0 auto;
}
#content {
padding: 0 10px;
background: url(images/content.png) repeat-y;
}
#content ul {
float: left;
display: block;
width: 450px;
margin: 0;
padding: 0;
background: url(images/networks.png) repeat;
list-style: none;
}
#content li {
float: left;
height: 60px;
width: 225px;
position: relative;
}
#content a {
font-size: 1em;
color: #666;
text-decoration: none;
padding: 10px 0 5px;
display: block;
width: 225px;
height: 45px;
position: relative;
z-index: 2;
}
a img {
border: 0;
float: left;
margin: 0 10px;
}
.fader {
opacity: 0;
-moz-opacity: 0;
filter:alpha(opacity=0);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 60px;
background-color: #A4ADB7;
}
a:hover img {
margin-left: 5px;
}
a strong {
display: block;
font-size: 18px;
color: #000;
}
.clear {
clear: left;
}
#datacontent {
padding: 20px 10px;
}
#datacontent ul {
list-style: none;
margin: 0;
padding: 5px;
display: block;
height: 30px;
background: #DFDFDF;
-moz-border-radius-topleft: 10px;
-moz-border-radius-topright: 10px;
-webkit-border-top-left-radius: 10px;
-webkit-border-top-right-radius: 10px;
}
#datacontent li {
float: left;
}
#datacontent li a {
outline: 0;
display: block;
background: #DFDFDF;
color: #666;
text-decoration: none;
padding: 1px 5px;
margin: 5px;
border: 1px solid #DFDFDF;
}
#datacontent li a:hover {
border: 1px solid #D1D1D1;
background: #D1D1D1;
color: #000;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
}
#datacontent li a.selected {
background: #AFAFAF;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
border: 1px inset #fff;
color: #fff;
}
#datacontent .panels {
border: 1px solid #DFDFDF;
border-top: 0;
border-bottom: 10px solid #DFDFDF;
-moz-border-radius-bottomleft: 10px;
-moz-border-radius-bottomright: 10px;
-webkit-border-bottom-right-radius: 10px;
-webkit-border-bottom-left-radius: 10px;
overflow: hidden;
}
#datacontent .panelsInner {
overflow: hidden;
}
#datacontent .panel {
padding: 20px;
}
</style>
<!--[if IE]>
<style type="text/css" media="screen">
div,
li {
zoom: 1;
}
</style>
<![endif]-->
</head>
<body>
<div id="wrapper">
<div id="content">
<ul>
<li><a href="http://twitter.com/maxvoltar"><img src="images/twitter.png" /><strong>Twitter</strong> twitter.com/maxvoltar</a></li>
<li><a href="http://flickr.com/photos/maxvoltar"><img src="images/flickr.png" /><strong>Flickr</strong> flickr.com/photos/maxv…</a></li>
<li><a href="http://twitter.com/rem"><img src="images/twitter.png" /><strong>Twitter</strong> twitter.com/rem</a></li>
<li><a href="http://flickr.com/photos/maxvoltar"><img src="images/flickr.png" /><strong>Flickr</strong> flickr.com/photos/maxv…</a></li>
<li><a href="http://twitter.com/jquery"><img src="images/twitter.png" /><strong>Twitter</strong> twitter.com/jquery</a></li>
<li><a href="http://flickr.com/photos/maxvoltar"><img src="images/flickr.png" /><strong>Flickr</strong> flickr.com/photos/maxv…</a></li>
<li><a href="http://twitter.com/jeresig"><img src="images/twitter.png" /><strong>Twitter</strong> twitter.com/jeresig</a></li>
<li><a href="http://flickr.com/photos/maxvoltar"><img src="images/flickr.png" /><strong>Flickr</strong> flickr.com/photos/maxv…</a></li>
</ul>
<div class="clear"></div>
</div>
<div id="datacontent">
<ul class="navigation">
<li><a href="#networks">Networks</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<div class="panels">
<div id="networks" class="panel">
<h2>Networks</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
<div id="about" class="panel">
<h2>About</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
<div id="contact" class="panel">
<h2>Contact</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
</div>
</div>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript" charset="utf-8">
$('#content a').each(function () {
$('<div class="fader" />').css('opacity', 0).prependTo(this);
}).hover(function () {
$('img', this).stop().animate({
marginLeft : 5
}, 250);
$('.fader', this).stop().animate({
opacity : 0.15
});
}, function () {
$('img', this).stop().animate({
marginLeft : 10
}, 250);
$('.fader', this).stop().animate({
opacity : 0
});
}).find('img').css('marginLeft', 10);
$('.navigation').each(function () {
var $links = $(this).find('a'),
panelIds = $links.map(function() { return this.hash; }).get().join(","),
$panels = $(panelIds),
$panelwrapper = $panels.filter(':first').parent(),
delay = 500,
heightOffset = 40; // we could add margin-top + margin-bottom + padding-top + padding-bottom of $panelwrapper
$panels.hide();
$links.click(function () {
var link = this,
$link = $(this);
// ignore if already visible
if ($link.is('.selected')) {
return false;
}
$links.removeClass('selected');
$link.addClass('selected');
document.title = 'jQuery look: Tim Van Damme - ' + $link.text();
if ($.support.opacity) {
$panels.stop().animate({opacity: 0 }, delay);
}
$panelwrapper.stop().animate({
height: 0
}, delay, function () {
var height = $panels.hide().filter(link.hash).css('opacity', 1).show().height() + heightOffset;
$panelwrapper.animate({
height: height
}, delay);
});
});
$links.filter(window.location.hash ? '[hash=' + window.location.hash + ']' : ':first').click();
});
</script>
</body>
</html>

Play QuickTime version
Play Flash version

Chris Mahon On 20th February 2009 at 15:02
Nice work Remy, although recommend maybe using the noscript tag to make it look less jumpy on first load.
Add display: none; to #datacontent .panel
Then add this somewhere:
Markus On 20th February 2009 at 15:02
Hey Remy, there seems to be an issue with the podcast on iTunes.
http://jqueryfordesigners.com/media/timvandamme.mp4 (working) vs http://jqueryfordesigners.com/media/timevandamme.mp4 (on iTunes)
Remy On 20th February 2009 at 15:02
@Markus - cheers - there was a typo in the itunes.xml feed. I’ve fixed this, hopefully iTunes won’t take too long to correct it.
todd On 20th February 2009 at 15:02
Great tutorial. I enjoy the “mistakes” and they are incredibly helpful in learning how to program / debug.
Markus On 20th February 2009 at 15:02
Working now, thanks a bunch!
Smukkekim On 20th February 2009 at 16:02
Thanks for another excellent post! There were some great tips in there. Don’t beat yourself up over the mistakes :-) In fact, when you apologize for wasting time, I couldn’t disagree more. Whenever I recommend your site to friends (often!), that is actually one of my greatest selling points - the fact that you don’t deliver a completely polished product. When you make a mistake, you just keep going, and there’s a lot to be learned from watching your debugging-process. (Another selling point, is that you always offer a flash-alternative to the quicktime-movie)
Joe McCann On 21st February 2009 at 05:02
Very slick use of the map() method. Never thought about it that way.
Kirill Galenko On 21st February 2009 at 14:02
Thank you so much man, I was just looking for an effect like “accordion bounce” to use on my new site :)
This is perfect.
whateverDesign On 22nd February 2009 at 17:02
Nice dissection of Tim’s website. This guy here uses some similar effects which seem to work perfectly cross-browser compatible, like the hover shift: http://matthiaskretschmann.com Also somehow the tab functionality there works even without JavaScript enabled.
k3k On 24th February 2009 at 18:02
Another great tutorial. I think I will use something like this in one my new work, so thank you a lot.
vache On 26th February 2009 at 00:02
hey great tutorial, i am not the smartest cookie so please go easy on me, i am trying to have height equal your heightOfset + the height of the post or another
you see i am using wordpress with the infinite-scroll plugin, now what happens is the panel loads fine that is the first page of the posts when you scroll it does not increase in height, so what can i do to have height get the height attribute from he class post and add that so when the wordpress loop runs with the infinite-scroll adding pages of posts it will keep adding the post’s height to the your “height” parameter
pepperpot On 27th February 2009 at 19:02
Thanks so much! You have no idea how much you’ve been helping me out… especially with the process. I agree with Todd and Smukkekim… it’s the uneditedness, watching you think through everything that make your tutorials so valuable.
Jonathan Dba On 1st March 2009 at 21:03
Thank you soo much Remy for picking up my request. You rock and you’ve answered so many of my questions.
howie On 2nd March 2009 at 16:03
This tutorial has been wonderful and made me want to combine it with one of your other ones for a cool effect.
Is there a way to make the script work where you can house the links in the codaslider on multiple panels and then use the different links to call the accordion? I’ve tried to make this work, but it appears to log the two sets of links separately, which breaks the navigation and the effect.
I would appreciate any help you could give. I’m still very new with javascript and jquery.
The test site is at http://www.mountpisgahstudentlife.org/staff/index2.shtml
brian On 10th March 2009 at 20:03
great tutorial! anyone have any thoughts on how to correct the aliased text that appears in the panel when viewing in IE7?
brian On 10th March 2009 at 20:03
figured out how to fix the aliasing that occurs in IE7… find #datacontent .panel and replace the whole like with the following:
datacontent .panel { display:none; padding:0; margin:0; background-color: #fff; }
this worked for me anyway… cheers!
deb On 11th March 2009 at 16:03
in ie7 renders realy pixelated. so does
<b>tag in the panel div copy.Ryan On 27th March 2009 at 12:03
Thanks. Brian, I’ve been needing that aliasing solution for awhile now.
joão On 3rd April 2009 at 13:04
Thank you so much! That’s exactly what I was looking for my university project at http://labs.sapo.pt/ua/sapohey - thank you SO much! :)
tony petruzzi On 30th April 2009 at 13:04
excellent tutorial! i also want to say that although the tutorial got off to a rough start, i found that part EXTREMELY helpful since it showed how to use firebug and console.log to debug a problem. like any geek, watching people work through problems is invaluable learning material.
Nathan On 7th June 2009 at 01:06
isn’t it so ironic that I found Tim’s site just yesterday while looking through bookmarks on delicious. and then today I come across jqueryfordesigners.com for the first time ever and find this tutorial without the intention of searching for it.
Greg On 27th July 2009 at 20:07
Something weird going on in Firefox 3.0.1.2 for Windows, in both your demo and when I move the code over to my test site. If you’re clicking from link to link, sometimes it will “hang” and fail to update the contents of the div. I’m not sure what exactly, but there’s a timing issue of some sort.
On my test site, I didn’t even have the content animated yet; it was simply the selecting of the panel div that caused the pause. Click on the link, pause (seems like browser is hung), then the div eventually updates.
Still, a highly interesting tutorial!
Greg
R.Bhavesh On 4th August 2009 at 14:08
Thanks a lot for sharing this awesome tutorial. I created a WordPress theme based on this. Take a look here and download http://tr.im/voqK
Pavel Ciorici On 30th August 2009 at 10:08
Thank’s for amazing tutorial!
But how about validating it’s code?
Do you have any valid option for this code? http://screensnapr.com/u/xnl3qo.png
JC On 7th September 2009 at 23:09
Great tutorial thanks. But what if you wanted to place a link outside of the navigation div? I’m struggling with that…. : ( (novice warning)