Site Meter
 
 

Animating lists with CSS 3 transitions

Ever wanted to implement animated lists or grids, like in the following 12-second video?

This may look like a native iOS app, but this UI is all implemented with HTML, CSS, and JavaScript (see the previous post for details about my new iPhone app, Touralot, built with PhoneGap). So, how does all this slidey stuff work?

Using “transform” and “transition” in lists

The basic principles here are:

  • For animations to be smooth and hardware-accelerated, they have to be done with transform: translate3d
  • Even so, element positions should still be determined by the DOM structure (minimizing use of JavaScript)

So, consider some markup like the following:

<ul class="items">
    <li>Monday</li>
    <li>Tuesday</li>
    <li>Wednesday</li>
    ...
</ul>

The element positions are determined by the DOM structure, but not using translate3d. Now consider the following CSS rules:

.items { position: relative; }
.items li { 
    position: absolute; top: 0; left: 0;
    transition: all 0.2s ease-out;
}
 
.items li:nth-child(1)  { transform: translate3d(0, 0%, 0); }
.items li:nth-child(2)  { transform: translate3d(0, 100%, 0); }
.items li:nth-child(3)  { transform: translate3d(0, 200%, 0); }
...

As you can see, the <li> elements will now be positioned absolutely against the parent <ul>, and we use translate3d to recover the usual vertical offset based on each element’s position in the DOM. What’s more, there’s a transition rule so that, whenever translate3d changes, the browser smoothly animates the element to its new position.

[Aside: For Webkit, you’d need prefixed rules as well (-webkit-transform and -webkit-transition). Current versions of IE and Firefox don’t require the prefixes.]

What’s the result? Well, now whenever you change the set of <li> elements inside the <ul>, all the other <li> elements will smoothly animate into their new positions, without you having to write any code to tell them to do so. This works wonderfully if you’re generating your DOM through any kind of templates/binding library such as with Knockout’s foreach binding, but also works nicely just with plain old DOM operations. Try this:

The add and remove functionality doesn’t have to tell the elements to animate. They just do that naturally. For example, the code for add is just:

$(".append").click(function () {
    $("<li>New item</li>").insertAfter($(".items").children()[2]);
});

Browser quirks

Webkit (or at least Chrome) has annoying bug whereby it doesn’t account for in-flight CSS transitions when computing the scroll extents of a container. So you’ll need to do something like the following to force it to recompute the scroll container bounds at the end of the transition:

// Workaround for Webkit bug: force scroll height to be recomputed after the transition ends, not only when it starts
$(".items").on("webkitTransitionEnd", function () {
    $(this).hide().offset();
    $(this).show();
});

Also, since IE<10 doesn’t support transform or transition, you might also want to include some conditional CSS like the following to get basic non-animated behaviour on older browsers:

<!--[if lte IE 9]><style type="text/css">
    /* Old IE doesn't support CSS transform or transitions */
    .list-example .items li { position: relative; display: inline-block; }
</style><![endif]-->

It works for grids, too

With this technique, there’s nothing special about one-dimensional vertical lists. It works exactly as well for two-dimensional grids, without any changes.

Well actually, this raises the question of where all those .items li:nth-child(x) rules are coming from. So far I assumed you were writing them by hand. But how many such rules should you write? Couldn’t we generate them programmatically? Why yes, of course. Try this utility function:

function createListStyles(rulePattern, rows, cols) {
    var rules = [], index = 0;
    for (var rowIndex = 0; rowIndex < rows; rowIndex++) {
        for (var colIndex = 0; colIndex < cols; colIndex++) {
            var x = (colIndex * 100) + "%",
                y = (rowIndex * 100) + "%",
                transforms = "{ -webkit-transform: translate3d(" + x + ", " + y + ", 0); transform: translate3d(" + x + ", " + y + ", 0); }";
            rules.push(rulePattern.replace("{0}", ++index) + transforms);
        }
    }
    var headElem = document.getElementsByTagName("head")[0],
        styleElem = $("<style>").attr("type", "text/css").appendTo(headElem)[0];
    if (styleElem.styleSheet) {
        styleElem.styleSheet.cssText = rules.join("\n");
    } else {
        styleElem.textContent = rules.join("\n");
    }
}

You can specify a maximum number of elements to account for, and the number of columns desired in your grid (or pass 1 for a single-column vertical list). Of course, it would be nicer still if we could write just one rule that specified the translate3d values as a function of the element index, but I’m not aware of any way of doing that in CSS 3. Let me know if you can think of one!

Here’s the result, as a three-column grid:

Supporting arbitrary reorderings

The technique so far is great for animating all elements other than the one you’re inserting or reordering. But if you remove and insert an element in a new position, it will appear there instantly, without a transition, because it is a brand new element as far as the CSS transition logic is concerned.

So, how would it be possible to achieve something like the following, where the “reorder” button smoothly moves elements to new positions? Try it: click the random order button:

One possible technique is to override the translate3d values for each element with a snapshot of their current values, so that the elements retain their coordinates independently of their DOM order. Then, after mutating the DOM, remove your snapshot values, and then the CSS transition will kick in to move the element smoothly to its final location.

Here’s are a couple of handle jQuery utility functions:

(function () {
    var stylesToSnapshot = ["transform", "-webkit-transform"];
 
    $.fn.snapshotStyles = function () {
        if (window.getComputedStyle) {
            $(this).each(function () {
                for (var i = 0; i < stylesToSnapshot.length; i++)
                    this.style[stylesToSnapshot[i]] = getComputedStyle(this)[stylesToSnapshot[i]];
            });
        }
        return this;
    };
 
    $.fn.releaseSnapshot = function () {
        $(this).each(function () {
            this.offsetHeight; // Force position to be recomputed before transition starts
            for (var i = 0; i < stylesToSnapshot.length; i++)
                this.style[stylesToSnapshot[i]] = "";
        });
    };
})();

Now you can achieve the random reordering thing as follows:

$(".reorder").click(function () {
    $(".items li")
        .snapshotStyles()
        .tsort({ order: "rand" })
        .releaseSnapshot();
});

This works just the same with two-dimensional grids as with vertical lists.

For convenience, I used the tinysort library here (I only just learned about it this morning – it’s very neat), which can also sort the elements in a meaningful way, not only randomly. But it would work exactly the same if you write manual code to reorder the elements.

Supporting drag-and-drop reordering

Getting to this point may have seemed complicated, but it has some great benefits. Since the animation and positioning is controlled entirely by CSS, it composes beautifully with many other techniques for modifying the DOM. For example, if a drag-drop library shuffles the DOM elements, then they will now animate.

As an example, we can throw in jQuery UI’s sortable mechanism to enable drag-drop reordering. But let’s make it slick, and also animate the “dropping” phase of the operation, where the element you dragged moves from wherever you’re holding it into its final position.

Here’s how you can do that:

$("ul.items").sortable({
    start: function (event, ui) {
        // Temporarily move the dragged item to the end of the list so that it doesn't offset the items
        // below it (jQuery UI adds a 'placeholder' element which creates the desired offset during dragging)
        $(ui.item).appendTo(this).addClass("dragging");
    },
    stop: function (event, ui) {
        // jQuery UI instantly moves the element to its final position, but we want it to transition there.
        // So, first convert the final top/left position into a translate3d style override
        var newTranslation = "translate3d(" + ui.position.left + "px, " + ui.position.top + "px, 0)";
        $(ui.item).css("-webkit-transform", newTranslation)
                    .css("transform", newTranslation);
        // ... then remove that override within a snapshot so that it transitions.
        $(ui.item).snapshotStyles().removeClass("dragging").releaseSnapshot();
    }
});

Here’s the result. Be sure to try this in a desktop browser, not a phone/tablet, because jQuery UI sortable doesn’t handle touch events by default. Also if you’re on IE, be sure to use IE10+ or you won’t see animations.

Weird flicker in Chrome? It only happens when the example is in this iframe. Try the “Edit in JSFiddle” link to see it without.

… and here’s a two-dimensional grid, with the same drag-drop code:

If you want better support for phones and tables, there are various ways of upgrading jQuery UI “sortable” to respond to touch events. Or write your own.


20 Responses to Animating lists with CSS 3 transitions

  1. This is simply fantastic. thanks you so much for sharing this!

  2. John Arheghan

    Love the way you use HTML5 for creating the UI for the mobile device.
    Thanks a lot!!

  3. Martin Andersen

    After some years with web development, I am back to WPF programming, and it’s so last year (:- Keep up all the good work you are doing for the community.
    It’s very inspiring to see you and rob eisenberg moving? from wpf to web programming, it’s simple amassing what has happen in the last 2 year.

    Thanks

  4. Nick

    Steve, you never fail to impress! Looks fantastic, need to spend some time digesting all this. It seems quite an innovative technique, is there any prior art or inspiration that you based this off?

  5. sushant

    Its really interesting to get all this work
    I have made scrolling grid with fixed headers..
    Will share as fast as i can

  6. I think there are many people trying to figure out how to make awesome user experiences using web technologies in conjunction with mobile devices, and I suspect you’ve almost nailed it.
    Looking forward to more posts on what you have done.

  7. Pingback: Improving the web platform with animated UI | Brillskills

  8. Summer Weitman

    CSS is designed primarily to enable the separation of document content (written in HTML or a similar markup language) from document presentation, including elements such as the layout, colors, and fonts.This separation can improve content accessibility, provide more flexibility and control in the specification of presentation characteristics, enable multiple pages to share formatting, and reduce complexity and repetition in the structural content (such as by allowing for tableless web design). ..;

    With kind thoughts
    <http://www.foodsupplementcenter.com

  9. This is really cool, I was trying to do something similar but have the sorting band back to a knockout observableArray. What would you suggest would be the best way to do this? My solution was this http://jsfiddle.net/fHPa8/ is there a better way?

  10. Brett

    Excellent series on Knockout. Keep it up — we really appreciate you sharing what you’ve learned. -B

  11. Max

    Do you have any idea to improve this technique for supporting elements of differents size?

  12. So many great parts to this post. Will definitely use as reference at some point soon. Thanks for putting it together.

  13. We use browser software to read texts on a web page, to view an
    image, to fill a web form, to watch funny videos, to get connected with a friend in
    social site, to read emails and to look for new information on the web to satisfy our
    curiosity. He feels Google will never survive against those browser companies.
    Disable this technology in the following way for solving this problem:.

    Here is my webpage: Google chrome xml [gralva.com]

  14. Pingback: Revision 128: CoffeeScript, mobile Apps, Gewinne | Working Draft

  15. I’ve read a few good stuff here. Certainly price bookmarking for revisiting. I wonder how much effort you place to make the sort of magnificent informative site.

  16. Please let me know if you’re looking for a article author for your blog. You have some really good posts and I believe I would be a good asset. If you ever want to take some of the load off, I’d love to write some material for your blog in exchange for a link back to mine.
    Please shoot me an e-mail if interested. Cheers!

  17. Greetings! Very helpful advice within this article!
    It’s the little changes that produce the biggest changes. Thanks a lot for sharing!

  18. G

    Great post! Looking forward to more useful stuff like this. Quick note: the “Random order” buttons in the samples are not working, apparently because the copy of tsort you linked to (http://tinysort.sjeiti.com/src/jquery.tinysort.min.js) is not available anymore (fails with a 404 error)

  19. Csaba Fabian

    Thanks for the great ideas! See below how I generated the css with lesscss (v1.4). 100 is a slight exaggeration, I will probably use a lot less in production. :)

    .translate3d (@x, @y, @z) {
    -webkit-transform: translate3d(@x, @y, @z);
    -moz-transform: translate3d(@x, @y, @z);
    -ms-transform: translate3d(@x, @y, @z);
    -o-transform: translate3d(@x, @y, @z);
    transform: translate3d(@x, @y, @z);
    }

    .loop (@index) when (@index > 0) {
    .comments li:nth-child(@{index}) {
    .translate3d(0, (@index – 1) * 100%, 0);
    }

    .loop (@index – 1);
    }

    .loop (0) {}
    .loop (100);

  20. Pingback: Links from the summer | developer52