Watch
Watch iPhone-like Sliding Headers screencast (Alternative flash version)
QuickTime version is approximately 68Mb, flash version is streaming.
View the demo used in the screencast
The Effect
I can’t show you an example in the wild so the screenshot below will have to do for now. As the user swipes through a list, the header for that section of content remains visible at the top of the window. This gives the user context in what they’re looking at.
One really nice UI effect is when a new heading is just about to replace the existing heading, it pushes the existing heading out of view. It’s very subtle and only really visible if you perform the scroll slowly. Unfortunately, for now, we’re not replicating subtle part of the effect.
What we will do, is when the user is scrolling through an overflowing block of content, the heading for the visible block of content remain at the top. You can see the effect in action in the demo.
Creating the Effect
To create this effect we need to have a fixed position fake header sitting over the content. To do this we’re going have to mess around with the DOM using jQuery. We need to wrap the box with another box of the same height and width and give it position: relative
so that our fake header can make use of position: absolute
and appear to be fixed.
Now that this is sitting at the very top level, we need to bring the real headers up over the fake header, so that it appears as if they’re pushing the fake out of the way. To achieve this we’ll use a z-index
on the real headers that is higher than the fake header. However, this causes it’s own problems.
All the headers now have to be position: absolute
to really sit over the fake header, which also means we have to give the element a fixed height and width. This isn’t too much of a problem because we can get this information from the original.
Now that the headers are position absolute, the text that is sitting next to it falls flush against each other, because the newly positioned headers don’t flow in the document. To fix this we need to create a spacer element. In the screencast I mention this might be possible to simplify. You could duplicate the header and insert it after the original header before setting the position: absolute
. To be consistent with the screencast, I’ve stuck with creating the spacer.
Finally we need to attach an event handler to the containing box, to say when there is a heading that is exactly aligned with the top of the containing box, to switch in the text from that header in to the fake and move the fake’s z-index
to be at least one more than the current header (so it sits topmost).
jQuery
The jQuery job breaks down in to four parts:
- Collect the variables we’ll need for the effect
- Position and insert the fake header
- Tweak the headings to be positioned absolutely and create the spacer
- Bind the scroll event
The completed example is also available if you want to skip through each step.
Variables
We need to grab jQueryified versions of the container box and headers. We also need to create a clone of the first heading for the fake header. Finally we initialise a z-index
and store the top position of the container. At first it looks like .offset().top
would do, but we also need to factor in the margin-top
and border-top-width
, and this gives us the real top position.
Of course the whole thing is wrapped in the $(document).ready()
method to ensure the code only runs once the DOM is ready.
$(document).ready(function () {
var $container = $('#box');
var $headers = $container.find('h2');
var $fakeHeader = $headers.filter(':first').clone();
var zIndex = 2;
var containerTop = $container.offset().top +
parseInt($container.css('marginTop')) +
parseInt($container.css('borderTopWidth'));
Inserting the Fake Header
This is a pretty straight forward process:
- Wrap the container in a box, in my case I’ve reused the
box
class name so that it’s the same width and height, but more importantly:position: relative
- Set the CSS on the
$fakeHeader
variable - Inherit details from the first original header, such as the width and text
$container.wrap('<div class="box" />');
$fakeHeader.css({
zIndex: 1,
position: 'absolute',
width: $headers.filter(':first').width()
});
$container.before($fakeHeader.text($headers.filter(':first').text()));
Absolutely Positioning Headings
Since we’re absolutely positioning the headings we’ll need to manually reset the width of the element. We’re also setting a constantly incrementing z-index
that the fake header can borrow from to jump above the real heading.
Once the headings are absolutely positioning, they no longer affect the flow of the document, and the adjacent elements now sit flush against each other. Now we need to manually correct this issue just using a spacer element. I’ve created a new empty div
element and set the height and width to the outerHeight
and outerWidth
of the heading. It’s important that we select the outerHeight
rather than just height
because we need to include the margin around that element.
I’d suggest that if you’re using this technique in a live environment, you can either do it using code (as I have done in this example), or if you’re finding that it doesn’t match up 100%, you can create a class in your CSS that prepares that spacer, then apply the class to the newly inserted div
.
$headers.each(function () {
var $header = $(this);
var height = $header.outerHeight();
var width = $header.outerWidth();
$header.css({
position: 'absolute',
width: $header.width(),
zIndex: zIndex++
});
// create the white space
var $spacer = $header.after('<div />').next();
$spacer.css({
height: height,
width: width
});
});
Using the Scroll Event to Trigger the Effect
As the user scrolls the overflowing container element, we need to track where our fake header is, and once it passes underneath a real header, the fake header will match the text and use a high z-index
.
To achieve this, we bind a scroll event to the container element, and as it is being scrolled, we loop through the headings checking it’s top
position.
If the top
position is less than the top
position of the container (remember we included margin and border width to accurately ascertain this), then we copy that heading’s details across to the fake heading.
The effect that we achieve is that as the fake header passes under the real header, as soon as they’re in the same location visually on the page, the fake header pops over the real header giving the illusion that the heading is now locked in position.
$container.scroll(function () {
$headers.each(function () {
var $header = $(this);
var top = $header.offset().top;
if (top < containerTop) {
$fakeHeader.text($header.text());
$fakeHeader.css('zIndex', parseInt($header.css('zIndex'))+1);
}
});
});
That’s all we need. As you’ll see with any of these tutorials, we just need to break the task in to smaller tasks and apply the solutions a bit at a time.
Check out the final iPhone-like Sliding Headers demo and let me know if you implement this technique in a real web site in the wild.
A Note About IE
IE8 is fine and matches this effect perfect. Of course IE6 & IE7 have to put their boot in, but only a little. There’s one subtle difference in IE6 & 7 due to a bug in their z-index
stack model. The bug is that the fake header doesn’t pass underneath, but over the top. I’ve had people look at the effect in IE and say nothing’s wrong – but now that I’ve pointed it out to you, you’ll see where the error is.
I know playing with the position: relative
will help, but I’m not 100% sure how to fix this just yet. So I challenge you, dear reader, to see if you can get it working in IE7. Good luck – I have hope in you!
You should follow me on Twitter here I tweet about jQuery amongst the usual tweet-splurges!