Search

iPhone-like Sliding Headers

Posted on 10th September 2009 — The iPhone has a few unique UI features, one in particular are the static headings when you’re scrolling through a list, so you know the context of the content. We’ll see how to create this effect using jQuery.

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:

  1. Collect the variables we’ll need for the effect
  2. Position and insert the fake header
  3. Tweak the headings to be positioned absolutely and create the spacer
  4. 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:

  1. 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
  2. Set the CSS on the $fakeHeader variable
  3. 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!