How to build a scrolling list with jQuery

Sunday 7th February

This tutorial explains, step-by-step, how to use CSS and jQuery animations to build a simple ‘auto-scrolling’ vertical list.

Let’s be clear, here: the widget I’m about to put together isn’t exactly a stunning new advance in user interface design. But it does produce a nice final result, and you’ll probably learn a little about CSS and jQuery animations along the way.

Requirements

To make things more interesting, we’ll give ourselves some pretty strict requirements about behaviour and display. Many similar devices I’ve seen on the web fail on at least one of these requirements, and this was part of the driver towards putting this tutorial together.

These requirements should make for an interesting enough challenge, whilst offering a reasonable use case from which to study CSS and jQuery. You can check the final demo to verify these requirements, and ensure your understanding of them is correct.

Setup

First, let’s get to the markup, which should be as straightforward as possible:

<ul id="scroller">
    <li>Item one</li>
    <li>Item two</li>
</ul>

I’ve added some default CSS, for the purposes of this demo, in order to demonstrate how to resolve issues relating to spacing caused by any padding and margins present:

#scroller { list-style: none; padding: 1em;
    border: 1px solid #9DB029; margin: 1em 0; }

#scroller li { border: 1px solid #ddd; width: 8em;
    margin: 0.25em; padding: 0.5em; background-color: #eee; }

And here’s how the basic list is displayed:

Adding the new item

The first step, of course, is to add the new item at the beginning of the list. To do this, we’ll use jQuery’s prepend() function:

$('#scroller').prepend('<li>A new item</li>');

which adds a list item as the first child of the list, ensuring it appears right at the top:

This demonstrates the first problem: our list will expand its height to accommodate the items contained within it. We’ll fix that simply by assigning a fixed height to the list, for the duration of our animation:

$('#scroller').css('height', $('#scroller').height());

Unfortunately, the last item now ‘spills out’ of the list, something we definitely don’t want to be happening. This won’t be a problem for now, because we’re going to move everything up so that the original items occupy their original positions, but we’ll have to revisit the ‘overflow’ problem soon.

For now, let’s move everything up by applying a negative margin to the new item:

The size of this margin is simply the total height that the item was occupying, which includes its content height, padding, and borders. Note that it does not include any vertical margins, since they collapse. Thanks to a built-in jQuery function, we can easily calculate this height, and set the margin:

$('#scroller li:first').css('margin-top',
    0 - $('#scroller li:first').outerHeight());

Now we really do have to deal with the overflow problem. Fortunately, it’s very easy to clip the overflowing content using the overflow CSS property:

$('#scrolling').css('overflow', 'hidden');

Note that this applies to the entire list, so it will handle any later situation in which the final list item might overflow the bottom.

Finally, for this initial stage, we need to slide the new item up a bit, just out of view. For this calculation, it’s important to realise why that part of the box is still showing: the list’s padding. Recall that we moved the new item up such that the original top item was back in its starting place. Therefore, we need to move our new item up by the same amount of top padding on the list.

The other factor to consider is that we don’t want anything else to be affected by this movement, in particular, we don’t want the following list items to move up. Relative positioning is the way to move an element whilst ensuring others respect its original position:

var ulPaddingTop = $('#scroller').css('padding-top');
    ulPaddingTop = ulPaddingTop.substr(0, ulPaddingTop.length - 2);
$('#scroller li:first').css('position', 'relative');
$('#scroller li:first').css('top', 0 - ulPaddingTop);

Note that, slightly frustratingly, the value returned for padding-top includes the trailing “px”, so that needs to be stripped with a call to substr().

It’s as if our new list item isn’t even there …

Moving the new item into place

Now we’ve got the new item in the list, but hidden out of view, it’s time to move it into position. First of all, let’s slide it down back to where it was before the previous step, by setting the top back to 0:

$('#scroller li:first').animate({top: 0});

You should see the effect of that step when you click the button above. Next, we’ll undo the other initial positioning that was applied to bring the new item fully into view:

var oldMarginTop = $('#scroller li:first').css('margin-top');
...
$('#scroller li:first').css('margin-top',
    0 - $('#scroller li:first').outerHeight());
...
$('#scrollerli:first').animate({marginTop: oldMarginTop});

Note that the first step — storing the original value of the item’s top margin — needs to take place just after we added it since it needs to be maintained when the item is moved back into its original position:

Again, clicking the button should demonstrate this step’s effect. All that remains is to get rid of the last list item.

Removing the old item and cleaning up

In much the same way that we originally moved the new item up past the list’s top padding, we now need to push the last item below the list’s bottom padding. Once that’s been done, the unwanted item needs to be properly removed from the DOM:

var ulPaddingBottom = $('#scroller').css('padding-bottom');
    ulPaddingBottom = ulPaddingBottom.substr(0, ulPaddingBottom.length - 2);
$('#scroller li:last').animate({top: ulPaddingBottom});
$('#scroller li:last').remove();

And, finally, we should undo any changes we’ve applied to the list to ensure it’s back in its original state:

$('#scroller').css('height', 'auto');
$('#scroller').css('overflow', 'visible');

Apart from being nice and tidy, this helps to minimise any disruption caused by possible changes in text size, caused by the user zooming text, for example.

Putting everything together

Before we finish, let’s consider how to run the animation parts of the process. Ideally, we want those steps to happen in order; if several elements move all at once, it can be very distracting. However, if adding the new item causes the overall list’s height to change, the effect works best if that animation happens at the same time as the other steps.

jQuery’s animate() function takes an optional duration parameter, which can be specified in milliseconds to define how long the animation runs for. In addition, a callback function can be provided to ensure steps in an animation are carried out in sequence.

.animate( properties, [ duration ], [callback] )

Firstly, we calculate any height difference the item change might cause to the overall list. This is simply the difference in the height of the old and new list items:

var heightDiff = $('#scroller li:first').outerHeight()
    - $('#scroller li:last').outerHeight();

Then, we run two animations in parallel: one for the list height change, and one for the items’ movement. That movement consists of 3 steps (show new item, scroll all items, hide old item) which should, in total, take the same amount of time as the height change. I’ve found the following values to be pleasant but, of course, you can experiment:

$('#scroller').animate({height: h + heightDiff}, 1500);

$('#scroller li:first').animate({top: 0}, 250, function() {
  $('#scroller li:first').animate({marginTop: oldMarginTop}, 1000, 
    function() {
      $('#scroller li:last').animate({top: ulPaddingBottom}, 250, 
        function() {
          $('#scroller li:last').remove();
          $('#scroller').css('height', 'auto');
          $('#scroller').css('overflow', 'visible');
      });
  });
});

And here’s the final effect; each click on the button adds a new item:

The code is wrapper up in a smoothAdd() function which takes a list’s id and text to add within a new list item.

And finally …

The eagle-eyed amongst you might spot one, very small problem with the final effect. For now, I’ll leave that identification as an exercise for the reader, in the knowledge that I’m opening myself up to all manner of problem reports! But I’ll deal with the specific issue that I’ve spotted in a follow-up post shortly.


Comments

Mon 8 Feb 2010 05:47

Bartek Stankowski

Bartek Stankowski said:

That’s a nice step-by-step tutorial.

But I think it’s worth saying that you could improve performance and make your code cleaner, with a few simple changes.

First of all, you can apply more than one CSS rule at a time, so instead of writing:

<pre>$('#scroller li:first').css('position', 'relative');

$('#scroller li:first').css('top', 0 — ulPaddingTop);

</pre>

you should do just:

<pre>

$('#scroller li:first').css({

position: 'relative',

top: 0 — ulPaddingTop

})

</pre>

No need to look for $('#scroller li:first') in the DOM so many times.

Also, you could cache DOM nodes in variables, and than find it’s children using context, like this:

<pre>

var $scroller = $('#scroller');

$('li:first', $scroller);

</pre>

It’s faster.

And the last thing. When inside an animation callback function, you can use "this", to animate the same element again.

Instead of:

<pre>

$('#scroller li:first').animate({top: 0}, 250, function() {

$('#scroller li:first').animate({marginTop: oldMarginTop}, 1000, function() {

// (…)

})

})

</pre>

use:

<pre>

$('#scroller li:first').animate({top: 0}, 250, function() {

$(this).animate({marginTop: oldMarginTop}, 1000, function() {

// (…)

})

})

</pre>

Mon 8 Feb 2010 05:51

Bartek Stankowski

Bartek Stankowski said:

Oh and one more thing.

I think it’s easier to use parseInt() instead of substr() to get the padding value:

var ulPaddingBottom = parseInt($('#scroller').css('padding-bottom'));

Cheers.

Mon 8 Feb 2010 06:53

Five Minute Argument

Five Minute Argument said:

@Bartek: Those are great performance tips, thanks — I'll definitely look into working those into the example. As I stated, the focus was on the step-by-step tutorial, particularly with respect to the CSS positioning, so I didn’t focus on efficiency too much. But, if the animation is not performing well, it should certainly be improved — a smooth movement was one of the requirements, after all!

As an aside, does jQuery not carry out its own caching to speed up repeated selectors? I suppose it’s not really necessary, if you write decent code in the first place ;-)

And good tip on parseInt() — I always forget it can be used in that way.

Mon 8 Feb 2010 11:04

Amber Weinberg

Amber Weinberg said:

Nice tutorial :)

Leave a comment