Watch
Watch BBC Radio 1 Zoom Tabs screencast (Alternative flash version)
QuickTime version is approximately 60Mb, flash version is streaming.
View the demo used in the screencast
BBC Radio 1 effect

The first thing I noticed about the effect is that it’s done in Flash. I’m a big believer in knowing where Flash has it’s place: the high end of the effects within browsers (amongst other places).
Equally though, I can how this effect can be achieved using JavaScript and it not taking too much toll on the browser.
There’s three things that are happening in this effect:
- When you roll over the image, it zooms out slightly to reveal more of the image. Notice that it also keeps the top position static.
- The navigation slides in to view from the bottom of the box, and mousing over the link will trigger showing a different image.
- The images automatically change with a swipe effect.
I’m going to set aside the 3rd effect for now (perhaps I’ll do a screencast on it in the future) and focus on the first two.
Deconstructing the effects
The first thing I want to aim for is that the layout works without CSS, and the technique works without JavaScript enabled.
Roll over zoom
The only way we can get an image to appear to zoom is by having the actual image element and increasing the height and width. However, I’m going to create these image elements on the fly because when my page runs without JavaScript I want the images to appear in the background of the panels.
I’m still in two minds as to whether this should be a background image. The image certainly doesn’t add any value from a content point of view, so without CSS I think it looks right, but I’d be open to hear your opinions.
When these images are created they need to have their size increased by a defined percentage (which we’ll make a parameter), so that when we roll over, the image is resized down to it’s original size, giving the impression of a zoom out.
Given that we are going to stretch these images across the total height and width of the panel, it also means we need to wrap the content that’s initially in the panel in an element with a higher z-index to make sure the actual content can still be seen and used.
Navigation slide up
By default the jQuery slideUp method hides content, and slideDown reveals the content by expanding element downwards.
We need our navigation bar to reveal upwards. To achieve this we just need to position the navigation absolutely (which also helps our non-JavaScript version of the page), and use the animate method to animate the height from zero to the real height of the navigation item - which we’ll capture during initialisation.
HTML & CSS
The HTML for the solution is a pretty simple tab system, with the only slight exception that our tab panels are wrapped in a div:
<div class="zoomoutmenu">
<ul class="tabs">
<li><a href="#one">One</a></li>
<!-- etc -->
</ul>
<div class="panels">
<div id="one" class="panel">
<h2>Garden life</h2>
</div>
<!-- etc -->
</div>
</div>
As for the CSS, as an experiment, I’ve used EMs throughout the CSS - there’s no reason why you can’t use PXs. I was just interested in seeing the example being zoomed by the browser. I’ve set the body font-size to 62.5% so that 1em is equal to 10px.
The only slight deviation away from a traditional tab set up is the navigation is set at the bottom, and because I know with JavaScript we’re going to use absolutely positioned image elements, the ul.tabs has the following CSS position absolute and a z-index:
.tabs {
position: absolute;
bottom: 0;
z-index: 1;
}
Aside: Tabs without JavaScript
If you open look at the solution now, with JavaScript disabled, you’ll see that because we’ve wrapped our tab panels in a div with overflow: hidden, we’ve now created a tabbing system that works entirely without JavaScript.
If you wanted to use this in production: you need to be wary of ensuring the height of the panels is equal (since it is just overflowing) - otherwise this is a fairly clean way to tab content without the need for JavaScript
jQuery
There’s four jobs our zoom tabs (for want of a better name) needs to achieve:
- Convert background images to foreground images and apply styling to give them a default state of zoomed in
- Zoom the image inwards using an animation
- Slide the navigation up and down out of view
- Hovering over the navigation will change the tabs
I’m going to give you each part separately, then you can see the whole thing put together in the completed example.
Converting background images
As I’m trying to make a plugin, I’m saying that I don’t know anything about the images or height or width of the panels.
We need to loop through each panel, take the background image and create a new image with that url. Once that new image is loaded, we’ll pull out the height and width, work out the zoomed in height and width (which will be a percentage larger than the initial size), strip the panel of it’s background image and then insert the new image in to the panel. Phew! That’s sounds like a lot!
$.fn.zoomtabs = function (zoomPercent, easing) {
if (!zoomPercent) zoomPercent = 10;
return this.each(function () {
var $zoomtab = $(this);
var $tabs = $zoomtab.find('.tabs');
var panelIds = $tabs.find('a').map(function () {
return this.hash;
}).get().join(',');
var $panels = $(panelIds);
var images = [];
$panels.each(function () {
var $panel = $(this),
bg = $panel.css('backgroundImage').match(/urls*(["']*(.*?)['"]*)/),
img = null;
if (bg !== null && bg.length && bg.length > 0) {
bg = bg[1];
img = new Image();
$panel.find('*').wrap('<div style="position: relative; z-index: 2;" />');
$panel.css('backgroundImage', 'none');
$(img).load(function () {
var w = this.width / 10;
var wIn = w / 100 * zoomPercent;
var h = this.height / 10;
var hIn = h / 100 * zoomPercent;
var top = 0;
var fullView = {
height: h + 'em',
width: w + 'em',
top: top,
left: 0
};
var zoomView = {
height: (h + hIn) + 'em',
width: (w + wIn) + 'em',
top: top,
left: '-' + (wIn / 2) + 'em'
};
$(this).data('fullView', fullView).data('zoomView', zoomView).css(zoomView);
}).prependTo($panel).css({'position' : 'absolute', 'top' : 0, 'left' : 0 }).attr('src', bg);
images.push(img);
}
});
});
};
Let’s take a look at what’s happening in this code.
First of all, we’re dynamically collecting all the panel element ids using the map trick that I showed you in the last J4D episode:
var panelIds = $tabs.find('a').map(function () {
return this.hash;
}).get().join(',');
var $panels = $(panelIds);
The panelIds variable contains a string such as: #one,#two,#three - since this is the hash on the href the tab links, which we immediately capture as a jQuery instance.
Next, we loop through the $panels to convert the background images to foreground images.
bg = $panel.css('backgroundImage').match(/urls*(["']*(.*?)["']*)/),
This is a regular expression that captures the background image. It’s written to try to handle different cases in that the url could be plain, wrapped in single quotes or it could be wrapped in double quotes.
We test if we did successfully find the background image (which you may argue we don’t need since this plugin has a very specific purpose), and if it was found, we’ll create a new image.
Before we hook in to the image load event, we do two things: 1) remove the background image, and 2) wrap the panels contents in a div:
$panel.find('*').wrap('<div style="position: relative; z-index: 2;" />');
This is to ensure the contents appears above the image - specifically notice the z-index: 2.
The next line is 4 chained commands:
- Bind a load event to the image
- Prepend the image in to the panel - i.e. make it the first element
- Set the position of the image - which is overwritten as soon as the image is loaded (which I’ll come on to)
- Finally set the url of the image, which will in turn trigger the load event
The very last action we perform is to capture the image in the images array. This will be used later to zoom the images in and out.
Image load event
The image load event has 2 purposes:
- Set the zoomed in position via CSS (actually the last thing to happen)
- Store the CSS states of the full view and the zoomed view
Using a bit of math, we’re working out what the zoomed state is, and then both variables are stored against the image element using jQuery’s data function:
$(this).data('fullView', fullView).data('zoomView', zoomView).css(zoomView);
This means we can retrieve this data later on.
Zoom the image
I’ve created a separate function to handle the zooming in and out of the image. This is because it will be called from two different places with only one single different - which variable to use to set the CSS - either ‘fullView’ or ‘zoomView’ which we set just above.
function zoomImages(zoomType, speed) {
$(images).each(function () {
var $image = $(this);
if ($image.is(':visible')) {
$image.stop().animate($image.data(zoomType), speed, easing);
} else {
$image.css($image.data(zoomType), speed);
}
});
}
In this function we loop through the images that we captured in the images array. From there we say if the image is visible (i.e. it’s the currently active panel), then animate it’s CSS properties (either zooming out or in - this is defined by the zoomType).
If the image isn’t visible, we need to just change the CSS properties without the animation. This is because when the user switches to a different tab, the image must be in the same zoomed state as all the other panels.
Slide the navigation up and down
Next we need to say, if the user hovers over the whole thing, we want to:
- Trigger the
zoomImagesfunction - Slide the navigation up, or down if they’re hovering away
$zoomtab.hover(function () {
zoomImages('fullView', speed);
$tabs.stop().animate({ height : height }, speed, easing);
}, function () {
zoomImages('zoomView', speed);
$tabs.stop().animate({ height : 0 }, speed, easing, function () {
$(this).hide().dequeue();
});
});
The hover over is fairly simple, we stop the effect from running on the $tabs to prevent it from jumping and down madly, and we animate the height to a pre-captured height (which would happen in the initialisation phase).
Hover off is slightly different. We do the same, animating down to zero, but then we use a callback function to hide the navigation, this is to ensure it’s completely hidden once the effect has finished (because in my example, I’m using a border, and animating the height to zero would leave the border visible).
Hovering over navigation
Finally, since I’m replicating the effect on the BBC (certainly 2/3rds of it), I want to hover over the tabs to trigger the panel to switch. However, I don’t want it to trigger straight away, say if I accidently hover over a tab - so I’m using a bit of simple hover intent (you could well use the full plugin to achieve this though):
var hoverIntent = null;
$tabs.find('a').hover(function () {
clearTimeout(hoverIntent);
var el = this;
hoverIntent = setTimeout(function () {
$panels.hide().filter(el.hash).show();
}, 100);
}, function () {
clearTimeout(hoverIntent);
}).click(function () {
return false;
});
I’ve added a simple 100ms timeout to say if the user is over the tab, now show that panel.
Since I’m using setTimeout, I need to capture a copy of the current tab link, because the this keyword inside of the setTimeout is actually a reference to the window object.
Wrap up
With all my code now in place (there’s a few initialisation items I’ve not covered in this tutorial, but should be covered in the screencast the effect is now ready to be used.
Have a play with the zoom tabs effect, let me know what you think - particularly so if you think there’s a better name for it!
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 HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Radio 1 zoom tabs</title>
<style type="text/css" media="screen">
body {
background: #eee;
font-size: 62.5%;
font-family: helvetica, arial, sans-serif;
}
.zoomoutmenu {
border: 0.5em solid #fff;
position: relative;
height: 23.5em;
width: 50em;
margin: 0 auto;
}
.panels {
height: 23.5em;
width: 50em;
overflow: hidden;
}
.tabs {
margin: 0;
padding: 0;
position: absolute;
bottom: 0;
z-index: 1;
}
.tabs li {
float: left;
display: block;
width: 10em;
background-color: #fff;
text-align: center;
}
.tabs li a {
padding: 0.2em;
display: block;
text-decoration: none;
color: #000;
border-top: 5px solid #fff;
font-size: 1.3em;
}
.tabs li a:hover {
border-top: 5px solid #333;
background-color: #666;
color: #fff;
}
.panel {
background: #ccc;
padding: 1em;
height: 21.5em;
position: relative;
}
.panel h2 {
font-size: 3em;
color: #fff;
font-family: Garamond, times, serif;
padding: 1em;
margin: 0;
text-align: right;
}
#one {
background: url(images/radio1/one.jpg) no-repeat center center;
}
#two {
background: url(images/radio1/two.jpg) no-repeat center center;
}
#three {
background: url(images/radio1/three.jpg) no-repeat center center;
}
#four {
background: url(images/radio1/four.jpg) no-repeat center center;
}
#five {
background: url(images/radio1/five.jpg) no-repeat center center;
}
</style>
<script src="jquery-1.3.2.js" type="text/javascript"></script>
<script type="text/javascript" charset="utf-8">
$.fn.zoomtabs = function (zoomPercent, easing) {
if (!zoomPercent) zoomPercent = 10;
return this.each(function () {
var $zoomtab = $(this);
var $tabs = $zoomtab.find('.tabs');
var height = $tabs.height();
var panelIds = $tabs.find('a').map(function () {
return this.hash;
}).get().join(',');
$zoomtab.find('> div').scrollTop(0);
var $panels = $(panelIds);
var images = [];
$panels.each(function () {
var $panel = $(this),
bg = ($panel.css('backgroundImage') || "").match(/url\s*\(["']*(.*?)['"]*\)/),
img = null;
if (bg !== null && bg.length && bg.length > 0) {
bg = bg[1];
img = new Image();
$panel.find('*').wrap('<div style="position: relative; z-index: 2;" />');
$panel.css('backgroundImage', 'none');
$(img).load(function () {
var w = this.width / 10;
var wIn = w / 100 * zoomPercent;
var h = this.height / 10;
var hIn = h / 100 * zoomPercent;
var top = 0;
var fullView = {
height: h + 'em',
width: w + 'em',
top: top,
left: 0
};
var zoomView = {
height: (h + hIn) + 'em',
width: (w + wIn) + 'em',
top: top,
left: '-' + (wIn / 2) + 'em'
};
$(this).data('fullView', fullView).data('zoomView', zoomView).css(zoomView);
}).prependTo($panel).css({'position' : 'absolute', 'top' : 0, 'left' : 0 }).attr('src', bg);
images.push(img);
}
});
function zoomImages(zoomType, speed) {
$(images).each(function () {
var $image = $(this);
if ($image.is(':visible')) {
$image.stop().animate($image.data(zoomType), speed, easing);
} else {
$image.css($image.data(zoomType), speed);
}
});
}
$tabs.height(0).hide(); // have to manually set the initial state to get it animate properly.
// this causes opear to render the images with zero height and width for the hidden image
// $panels.hide().filter(':first').show();
var speed = 200;
$zoomtab.hover(function () {
// show and zoom out
zoomImages('fullView', speed);
$tabs.stop().animate({ height : height }, speed, easing);
}, function () {
// hide and zoom in
zoomImages('zoomView', speed);
$tabs.stop().animate({ height : 0 }, speed, easing, function () {
$tabs.hide();
});
});
var hoverIntent = null;
$tabs.find('a').hover(function () {
clearTimeout(hoverIntent);
var el = this;
hoverIntent = setTimeout(function () {
$panels.hide().filter(el.hash).show();
}, 100);
}, function () {
clearTimeout(hoverIntent);
}).click(function () {
return false;
});
});
};
$(function () {
$('.zoomoutmenu').zoomtabs(15);
});
</script>
</head>
<body>
<div class="zoomoutmenu">
<ul class="tabs">
<li><a href="#one">One</a></li>
<li><a href="#two">Two</a></li>
<li><a href="#three">Three</a></li>
<li><a href="#four">Four</a></li>
<li><a href="#five">Five</a></li>
</ul>
<div class="panels">
<div id="one" class="panel">
<h2>Garden life</h2>
</div>
<div id="two" class="panel">
<h2>Lego</h2>
</div>
<div id="three" class="panel">
<h2>Berlin</h2>
</div>
<div id="four" class="panel">
<h2>New York</h2>
</div>
<div id="five" class="panel">
<h2>Hungary</h2>
</div>
</div>
</div>
</body>
</html>

Play QuickTime version
Play Flash version

Zy On 3rd April 2009 at 12:04
Awesome!
Avaz Ibragimov On 3rd April 2009 at 13:04
Great tutorial. I’ll give it a try. Thx.
none On 3rd April 2009 at 14:04
Very interesting article… Amazing effect and no flash! Thanks!
Marvin On 3rd April 2009 at 15:04
oh man, thats just awesome!
mecaniqueorange On 3rd April 2009 at 18:04
really great… thanks :) i can’t wait the next tutorial with the third effect included ^^
Adam On 3rd April 2009 at 19:04
Awesome effect and tutorial Remy. Thanks!! -Adam
George On 3rd April 2009 at 20:04
Awesome stuff Remy! Question….It breaks in IE7. Wasn’t sure if you knew that?
Predrag Drljaca On 4th April 2009 at 16:04
Thank you very much for this tutorial.
dot tilde dot On 4th April 2009 at 23:04
asides from the technicalities, which where excellently explained as always, i struggled to find the ui element on the radio 1 homepage for about a minute or so.
as much as i like the ui element, i wouldnt hide important info behind if i expect impatient users.
.~.
Remy On 5th April 2009 at 10:04
@George - I did run tests in IE6 + 7, Opera and Safari to make sure it worked properly. Perhaps I missed something. I’ll go back to it and double check, and if there’s a fix required I’ll pop a comment up here.
@.~. - I’m inclined to agree - which I think why the ’swipe effect’ on the BBC page helps - because it gives a visual que that there’s more content behind this element.
derdude On 5th April 2009 at 11:04
Maybe one could show the menu on inital load and hide it after a given time.
Chris Greenhough On 6th April 2009 at 16:04
Hi Remy. This stuff just gets better and better. Until flash loses its bad rep and seo-unfriendly legacy, it’s amazing to see jQuery (and j4d, naturally) opening up this level of sophistication in UI terms. Just a quick question, would it be possible/better to only animate the image dimensions and allow that to push the link navbar down in sync? Or was there a specific reason for not doing it that way? I could see, I guess, that if you had a more extreme zoom the navbar would disappear pretty quickly, but I’m just curious. You’re creating the illusion that the whole block of content is revealed/hidden which is probably the case in the flash file…?
Tyrone Avnit On 13th April 2009 at 00:04
Love your blog…. This is where Universities fail and the internet blooms. This type of stuff you cannot learn anywhere else. Keep it up!
Arti On 16th April 2009 at 09:04
You are perfect designers. Wonderful!
kucrut On 26th April 2009 at 13:04
My God, just… awesome!
Dave Ellis On 27th April 2009 at 12:04
Nice tutorial, looking forward to having a go.
Andy On 2nd May 2009 at 22:05
Hi, Great tutorial.
How would you go about making the content slide from one item to the next so that its continuous…Like the radio 1 website does?
Gary F On 2nd May 2009 at 23:05
Nice in FF3, just a shame it doesn’t work properly in IE7 (the most used browser in the world).
Daniel Groves On 3rd May 2009 at 16:05
Very nice, will be used in an upcoming project!
Carl - Web Courses Bangkok On 4th May 2009 at 07:05
Ohh, I like this tutorial, thanks. Not found a use for it yet tho.
Carsten On 8th May 2009 at 11:05
Great tutorial, thanks!
The example breaks in IE7 and IE6 though, and the last tab is pushed below the others on Opera. I’ll post the code to solve it when I get to it.
BigAB On 11th May 2009 at 18:05
I copied and pasted the code from “View Code” and had IE break too. I just had to add position: relative; to the panels class to fix the IE thing.
This is great because it degrades gracefully but I agree there needs to be some sort of visual indicator that there is more content on hover. But that is easy enough to add to. Great work, this is quickly becoming one of my favourite and most frequented blogs.
DJ On 17th May 2009 at 01:05
@Remy… Yes, please do the white moving transition!
DJ On 17th May 2009 at 02:05
@remy…
Just a couple of suggestions for improving quality of your screencasts - for what they’re worth.
Do you have a way of acoustically isolating your microphone from your keyboard a bit better? It comes across as quite a bit of pounding by the time you’re finished - it does impress me, however, about how fast you type.
No need to spend time discussing failed attempts or cats. Although, a post about why so many web designers are housed in Oz might be nice.
I do quite like your organizational approach, i.e. your slash commented steps - in fact, wish other screencasters would follow suit. Sure would make them easier to follow and cut down on the off topic rambling.
sg On 25th May 2009 at 19:05
hi,
I might be missing something but I can’t figure out how to make the tabs actually point to urls. It seems they have hashvalues, but these point to the background images.
And if the panels simply point to the headings that sit on top of these background images, where do the urls to the tab links go???
cheers sg