Site Meter
 
 

Category: UI

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.


Full-height app layouts: Navigation and History

This is the third in a series of posts about web app layouts, i.e., giving your web-based application a UI comparable to a desktop or mobile/tablet app. Posts so far:

  1. Layout basics – A CSS technique for slicing up the browser window into arbitrarily nested panes both horizontally and vertically. You know, like a proper application, and not like an infinite-height document…
  2. Animated transitions – How to put more than one content block into a given pane, and then switch between them with hardware-accelerated animations
  3. This post – Keeping track of what content has appeared in each pane, so the visitor can navigate back and forwards. And supporting deep linking. And injecting new panes dynamically.

Disclaimer: To be clear, this post series just represents my own experiments in lightweight, flexible web app layouts and is not an official part of any Microsoft technology stack. No guarantees or warranties or SLAs, blah blah blah.

The goal

It’s very common on mobile/tablet-like UIs for users to navigate through some structure of content within a given pane. For example, in a tablet-like app, you’ll often want to have a left-hand pane representing navigation through a hierarchy, with the main section of the screen representing the currently selected item or folder:

image

So, you’ll want to be able to:

  • Keep track of where the user has been, letting them go back and forwards, with each pane able to maintain its own independent history.
  • Perform smooth animated transitions within each pane when any navigation event occurs.
  • Support deep-linking to arbitrary locations in your content structure
  • Fetch and render content dynamically

Basic navigation

Continuing from previous posts that introduced pane hierarchies and panes.js, it’s pretty easy to keep track of where a visitor has been. First you might set up a basic pane layout with a header/body/footer like this:

<div class="page">
    <div class="header row">
        Header will go here
    </div>
    <div class="body row">
        Body contents will go here
    </div>
    <div class="footer row">
        My footer. Could put icons here.
    </div>
</div>

… and then put multiple panes into the body row:

<div class="body row">
    <div id="location-continents" class="pane">
        <h3>Continents</h3>
        <ul>
            <li><a href="#america">America</a></li>
            <li><a href="#europe">Europe</a></li>
        </ul>
    </div>
 
    <div id="location-america" class="pane">
        <h3>Countries in the Americas</h3>
        <ul>
            <li><a href="#canada">Canada</a></li>
            <li><a href="#usa">USA</a></li>
        </ul>
    </div>
 
    <div id="location-canada" class="pane">
        <h3>Cities in Canada</h3>
        <ul>
            <li>Vancouver</li>
            <li>Toronto</li>
            <li>Edmonton</li>
        </ul>
    </div>
 
    <!-- ... etc ... -->
</div>

Now you can begin history tracking by adding a bit of JavaScript to instantiate a PaneHistory object and navigate to an initial pane:

// Navigation
var paneHistory = new PaneHistory();
paneHistory.navigate("location-continents");

… and perform pane navigations each time the visitor clicks on one of the links:

$("a").click(function (evt) { 
    var dest = (evt.srcElement || evt.target).href.split("#")[1]; 
    paneHistory.navigate("location-" + dest); 
    return false; 
});

This code will intercept clicks on the links, figure out which pane they are trying to get to, and then animate that destination pane sliding smoothly into the body area from the right.

If you also want to let the visitor go “back” through the pane’s history stack, add a button perhaps into the page header:

<div class="header row">
    <p><button class="goBack">&lt; Back</button></p>
</div>

… and handle clicks by instructing the PaneHistory instance to perform a reverse navigation animation:

$(".goBack").click(function () {
    paneHistory.back();
});

That’s it. Try it out. Live example:

Also: Run full screen (e.g., to try it on a phone)

Supporting the back/forward buttons and deep-linking

If your app will be deployed to the web (as opposed to using something like PhoneGap to package for an appstore), then you will almost certainly want to respect the browser’s native back/forward buttons and allow deep-linking to specific locations in your virtual navigation system.

There are many JavaScript libraries for working with browser history and the HTML5 pushState feature. Currently my favourite is history.js (license: New BSD) by Benjamin Lupton, because of its robustness, great pushState support, support for older browsers, and because it has no dependencies.

panes.js integrates with history.js, so once you’ve added a reference to history.js, you can start using UrlLinkedPaneHistory instead of PaneHistory. Specify the name of one or more URL parameters that the pane will use to represent what data it is showing. In this case, I’ll call my parameter “location”:

var paneHistory = new UrlLinkedPaneHistory({
    params: { location: 'continents' }
});

Then, update your paneHistory.navigate call so that it specifies which URL parameter is to be updated (in this case, my only parameter, “location”):

$("a").click(function (evt) {
    var dest = (evt.srcElement || evt.target).href.split("#")[1];
    paneHistory.navigate({ location: dest });
    return false;
});

And finally, since it no longer makes sense for “back” clicks to affect only a single pane (the URL history is global), update your “back” button handler so that it performs a global browser “back” navigation:

$(".goBack").click(function () {
    History.back();
});

… and you’re done. Now if you run the follow demo full-screen (click the link below the fake phone UI), you’ll see the URL update as you navigate around, and you can use the back/forward buttons (still getting the animated transitions), and can bookmark or refresh the page without losing your position. If your browser supports HTML5 pushState, for example recent versions of Chrome and Firefox, then your URLs will be updated with real querystring parameters as you go. If your browser doesn’t support pushState, history.js will gracefully degrade to using a URL hash.

Try it:

Note: If you want to be able to see the URL updating,
and to use the back/forward buttons, view this example full screen

Dynamically generating panes

If you do a view HTML source on either of the two previous demos, you’ll see that there’s a long list of pre-prepared <DIV> elements – one for each pane that you might visit (Europe, Canada, Vancouver, Toronto, etc….). That’s OK if your app only has a small and finite set of visitable panes (e.g., because those panes represent a fixed number of tabs), but it’s awkward if there are a lot of panes, and useless if the number is effectively infinite because panes represent navigation through data in some large external database.

Fortunately, this is easy to fix. Because panes.js doesn’t modify your DOM structure in any way, and requires no initialisation step to start using newly-inserted DOM elements, it composes perfectly with any external mechanism for updating the DOM. In this example, I’m going to use Knockout.js to inject new panes dynamically as data is fetched from some external source.

The trick here is to make the UrlLinkedPaneHistory a property of your Knockout view model. For example:

function AppViewModel() {
    this.paneHistory = new UrlLinkedPaneHistory({
        params: { location: null },
 
        // Making the set of history entries observable, so we can dynamically generate corresponding DIVs
        entries: ko.observableArray([]),
 
        // Each time the visitor navigates forwards, this will be called to fetch data for the new pane
        loadPaneData: function (params, callback) {
            // Here I'm loading data from some external source, asynchronously
            locationInfoService.getLocationInfoAsync(params.location, callback);
        }
    });
};
 
ko.applyBindings(new AppViewModel());

(Note: do a “view source” on the live example below if you want to see the finished code and what JS libraries you must reference to make all this work.)

Now the set of history entries is observable, we can ask Knockout to generate corresponding DIVs dynamically, by using a “foreach” binding:

<div class="body row" data-bind="foreach: paneHistory.entries">
    <div id="location-continents" class="pane scroll-y" data-bind="attr: { id: paneId }">
        <h3 data-bind="text: paneData.name"></h3>
    </div>
</div>

Notice that Knockout will assign an ID property to each pane <DIV> corresponding to the navigation parameters, so it can associate navigation events with the <DIV> you want to slide into view.

Of course, you don’t just want to display the “name” property of each item you’re visiting – you want to generate some more UI for each pane. Let’s generate a list of child locations that the user can navigate to:

<div class="body row" data-bind="foreach: paneHistory.entries"> 
    <div id="location-continents" class="pane scroll-y" data-bind="attr: { id: paneId }"> 
        <h3 data-bind="text: paneData.name"></h3> 
        <ul data-bind="foreach: paneData.children"> 
            <li data-bind="text: name, 
                            click: $root.navigate, 
                            css: { navigable: childLocations.length }"></li> 
        </ul> 
    </div> 
</div>

Each of the <li> elements there will try to invoke a viewmodel method called “navigate” when you click it. Implement that by adding a “navigate” method to your viewmodel:

function AppViewModel() {
    // Rest of class unchanged
 
    this.navigate = function (evt) {
        var destination = ko.dataFor(evt.srcElement || evt.target);
        if (destination.childLocations.length)
            this.paneHistory.navigate({ location: destination.id });
    } .bind(this);
};

Now the visitor will be able to navigate forwards through the hierarchy of locations. What about navigating backwards too? You can put a “back” button into the header pane, and use a Knockout binding to make its text update to show the name of parent location (e.g., “< Europe”):

<div class="header row">
    <p data-bind="with: paneHistory.currentData().parent">
        <button data-bind="click: $root.navigate">&lt; <span data-bind="text: name"></span></button>
    </p>
</div>

Done. Your visitor can now navigate through an arbitrarily deep hierarchy, with the data being pulled from the server as needed and dynamically rendered on the client. The browser’s back/forward buttons still work, deep linking works, and there are smooth animated transitions. And you wrote about 15 lines of JavaScript Smile. Have a go with it:

Note: If you want to be able to see the URL updating,
and to use the back/forward buttons, view this example full screen

What’s next?

Well, what are you interested in? Possible next posts in this series:

  • Using all this stuff to build a complete application with meaningful functionality
  • Making this kind of Single Page Application (SPA) work offline via HTML5 offline support
  • Packaging and selling such apps on mobile appstores via PhoneGap/Callback
  • Data access: editing collections of entities, and letting the user track, synchronise, and revert their changes

If you’re building single page applications, whether for mobile or desktop or both, please let me know what sort of technologies you’re using, what challenges you face, and what kinds of features you’d like to see baked into the ASP.NET/MVC stack.