Skip to main content
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Mark Drew
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Mark Drew

Experimenting With CSS-Based Animations And Transitions

By
Published in , Comments (11)

After seeing Ray Camden tweet this weekend about playing with jQTouch, I was inspired to revisit the jQTouch project myself, although from a slightly different angle. I've played with jQTouch before and found it to be awesome for a particular type of application; and, somewhat frustrating for everything else. Part of that frustration, I believe, stemmed from an ignorance of the technologies working just below the nicely-themed surface. As such, I wanted to take some time to look into the WebKit-Mobile-specific features that jQTouch abstracts away from the application developer. In much the same way that if it bleeds, we can kill it [Schwarzenegger, Predator], if we understand how it all works, we can change it.

I decided to start this exploration off with CSS-based animations, also known as CSS transitions. The first thing I discovered about CSS animations is that it was very hard to find any great documentation on how they worked or what kind of best practices exist. I was able to find a good number of demos; but, nothing that really explored the CSS animations in-depth. As such, this blog post will be more about exploration than it will be about explanation.

From what I can gather, there are two types of CSS-based animations: implicit and explicit. With an implicit animation, you tell the browser that anytime a particular property (or set of properties) changes, you want to implicitly animate to the new value. So for example, you can set an implicit linear animation on the Width of a box such that any time the width of that box changes, the browser will implicitly animate from the previous Width value to the new Width value. Explicit animations, on the other hand, specifically define "keyframes" that outline the logical path of the animation. So for example, rather than letting the browser animate from any previous Width to any new Width, an explicit animation would tell the browser to transition from a specific Width to another specific Width. In my mind, I make the distinction by thinking of the implicit animation as a "relative" transition and the explicit animation as an "absolute" transition.

This transition behavior does not exist for the entire life of a particular element; it only exists after a particular element has been defined as using transitions. This transition behavior can be detached and re-attached to a given element at any time, toggling implicit animations on and off. In the following code sample, I have defined my transition behavior with a CSS class:

div.animatedScreen {
	display: block ;
	-webkit-transition: left 1s ease-in-out ;
	}

Here, the "animatedScreen" class defines its associated element as using transitions. I can turn this behavior on and off simply be adding and removing this CSS class to and from any given element. As such, CSS property mutations will only cause implicit animations when the target element has this class bound to it. If this class is not bound to it, CSS mutations will take place instantly.

The "-webkit-transition" CSS property is actually short-hand for three different CSS properties:

  • -webkit-transition-property: This defines which CSS property mutations should result in implicit animations. In my case, only mutations of the "left" CSS property will cause animations. This value could also be "all" if you didn't want to limit the animations to a single property. NOTE: I could not figure out what CSS property could be used in conjunction with the translateX() transformation (henceforth removed from this demo).

  • -webkit-transition-duration: This defines how long the implicit animation should last. I have seen this represented as both seconds (ex. 3.5s) and milliseconds( ex. 350ms), but I am not aware of an exhaustive list of the various units of measurements.

  • -webkit-transition-timing-function: This defines the easing effect used to calculate the logical path of the animation (ex. linear, ease-in-out, etc.).

Because the animations are executed implicitly by the browser, rather than explicitly by the programmer, WebKit uses the event framework to signal animation events. In the following example, I am binding to the "webkitTransitionEnd" element event in order to determine when the implicit animation has finished executing.

Ok, let's take a look at some actual code. In the following demo, I have two "Screens" as represented by two DIV elements. Each DIV contains a link that will transition the other screen into view, much the same way in which the iPhone transitions into and out of menus.

<!DOCTYPE HTML>
<html>
<head>
	<title>CSS Transition And Animation Experiment</title>
	<style type="text/css">

		html,
		body {
			overflow-x: hidden ;
			}

		body {
			margin: 0px 0px 0px 0px ;
			padding: 0px 0px 0px 0px ;
			}

		div.screen {
			display: none ;
			left: 0px ;
			min-height: 300px ;
			position: absolute ;
			top: 0px ;
			width: 100% ;
			}

		div.currentScreen {
			display: block ;
			}

		div.animatedScreen {
			display: block ;
			-webkit-transition: left 1s ease-in-out ;
			}

		#screen1 {
			background-color: #333333 ;
			}

		#screen2 {
			background-color: #999999 ;
			}

		a {
			color: #FFFFFF ;
			display: block ;
			font-size: 40px ;
			height: 300px ;
			line-height: 300px ;
			text-align: center ;
			width: 100% ;
			}

	</style>
	<script type="text/javascript" src="jquery-1.4.2.min.js"></script>
	<script type="text/javascript">

		$(function(){

			// Get the screen references.
			var screen1 = $( "#screen1" );
			var screen2 = $( "#screen2" );

			// I am the flag to determine if the screens are
			// currently animating into position.
			var screensAreAnimating = false;

			// I am the flag to determine if Transitions are
			// supported (and more to the point, if the transition
			// event is supported.
			//
			// NOTE: This concept here taken directly out of the
			// jQTouch Javascript library.
			var screensCanAnimate = (typeof( WebKitTransitionEvent ) == "object");


			// Bind to the first link (to move screens left).
			screen1.find( "a" ).click(
				function( event ){
					// Check to see if the screen are animating. If
					// they are, we don't want to mess with them.
					if (screensAreAnimating){
						return;
					}

					// Flag screens as animating so we don't allow
					// multiple clicks to process.
					screensAreAnimating = true;

					// Position screen for animation.
					screen2.css( "left", "100%" );

					// Add animation class. This adds property
					// transitions to any subsequent CSS property
					// changes made to the screens.
					screen1.addClass( "animatedScreen" );
					screen2.addClass( "animatedScreen" );

					// Animate screens left - we need to do this
					// after a slight pause otherwise id doesn't
					// seem to work for both screens.
					setTimeout(
						function(){
							screen1.css( "left", "-100%" );
							screen2.css( "left", "0px" );

							// Check to see if the browser supports
							// animation. If not, we want to manually
							// trigger the end-animation event.
							if (!screensCanAnimate){
								screen1.trigger( "webkitTransitionEnd" );
							}
						},
						0
					);
				}
			);


			// Bind to the second link (to move screens right).
			screen2.find( "a" ).click(
				function(){
					// Check to see if the screen are animating. If
					// they are, we don't want to mess with them.
					if (screensAreAnimating){
						return;
					}

					// Flag screens as animating so we don't allow
					// multiple clicks to process.
					screensAreAnimating = true;

					// Position screen for animation.
					screen1.css( "left", "-100%" );

					// Add animation class. This adds property
					// transitions to any subsequent CSS property
					// changes made to the screens.
					screen1.addClass( "animatedScreen" );
					screen2.addClass( "animatedScreen" );

					// Animate screens left - we need to do this after
					// a slight pause otherwise id doesn't seem to
					// work for both screens.
					setTimeout(
						function(){
							screen1.css( "left", "0px" );
							screen2.css( "left", "100%" );

							// Check to see if the browser supports
							// animation. If not, we want to manually
							// trigger the end-animation event.
							if (!screensCanAnimate){
								screen1.trigger( "webkitTransitionEnd" );
							}
						},
						0
					);
				}
			);


			// Bind the end of the transition so we can remove the
			// animation-based classes and flag the system as not
			// being in an intermediary state.
			//
			// NOTE: Since we have both screens animating at the same
			// time, we only have to bind to one of them to know when
			// both of them have finished animating.
			screen1.bind(
				"webkitTransitionEnd",
				function( event ){
					// Remove animation class and set the current
					// screen. Since we only have two screens,
					// toggling the current screen should always
					// swap the screens appropriately after the
					// animation has completed.
					screen1.toggleClass( "animatedScreen currentScreen" );
					screen2.toggleClass( "animatedScreen currentScreen" );

					// Flag that we are no longer animating
					// the screens into position.
					screensAreAnimating = false;
				}
			);

		});

	</script>
</head>
<body>

	<div id="screen1" class="screen currentScreen">

		<a href="##">Go To Next Screen</a>

	</div>

	<div id="screen2" class="screen">

		<a href="##">Go To Previous Screen</a>

	</div>

</body>
</html>

As you can see, the transition from screen to screen requires a few basic steps:

  1. Position the hidden screen next to the visible screen. You'll notice that since the screens don't have implicit animations by default, this positioning takes place instantly.

  2. Attach the "animatedScreen" class to both the hidden and visible screens. By doing this, it will cause any subsequent mutations of the "left" CSS property to trigger an implicit animation as powered by the browser.

  3. Remove the "animatedScreen" class once the animation has finished.

You'll notice that after I have attached the animatedScreen class to the screen elements, I am updating the "left" CSS property from within a setTimeout() callback. I had to do this in order to give a slight pause between binding the CSS transition and actually making use of it (attempts to change the "left" CSS property directly after the class-binding would result in an instantaneous update). I have no idea why this is necessary and I would bet this is some sort of bug in the way the DOM gets updated. It certainly wouldn't be the first time that I've needed to incorporate setTimeout() callbacks into DOM alteration routines.

I am sure that my demo code above can be refactored a bit; but, it is clear that using implicit CSS-based animations is not an easier way of executing animations. In fact, I'd argue that using jQuery's animate() method would result in shorter, easier to understand, cross-browser compatible code. So, why would anyone choose to use proprietary WebKit CSS-based animations? Performance optimization. On mobile devices, like the iPhone, using native animations creates smoother, more power-efficient transitions thanks to hardware acceleration (whatever that actually means). Anyway, more to come on this topic as I try to figure out how it all works.

Want to use code from this post? Check out the license.

Reader Comments

92 Comments

This might sound Luddish, but if you have to trigger the whole thing with JavaScript anyway, why bother with CSS animations? Just seems like extra work for something that only functions in the most recent iterations of browsers.

Thoughts?

2 Comments

I've had a little play with the css3 animations but not really looked at what's actually going on underneath so was good to hear it in a more technical light.

Age old problem of IE of course, but to be honest I don't mind going to a little extra effort coding for people who made a little extra effort and downloaded a decent browser.

8 Comments

The setTimeout is needed because of the 'style-batching' that browsers do in order to make animating multiple properties at once more feasible. When you don't use setTimeout, all your different points get batched into the same group and you end up with immediate animation.

The webkit bug for this in transitions was filed here: https://bugs.webkit.org/show_bug.cgi?id=27159

The take away was the instead of using a setTimeout, you could call document.body.offsetWidth (perhaps even on a more specific element, depending on the situation). This forces the calculation, and stops it from being batched, though it can get expensive, so it shouldn't be done unnecessarily.

I know Thomas Fuchs and Andrew Dupont have been doing a crazy amount of work to get Scripty2 into a release version and they are using css transitions/transforms as the first line mechanism for animation. If you want to see a good implementation of a library using these features, and working through these bugs, I'd strongly suggest you look at Scripty2.

Bawler post again Ben. I can count on you to have done a few hour deep dive on something I'm interested in, and I can just come to your blog and work through all the weird stuff right away.

<3
Alex

15,902 Comments

@Andy,

While not related to my particular example, CSS-based animations provide a way to incorporate animations in situations where you don't have any Javascript included (ex. a link :hover effect). That said, I think the benefit here is not in brevity of the code, but rather in its performance - native CSS transitions are supposed to be smoother and more efficient on mobile devices.

8 Comments

@andy - the point of using JavaScript to control CSS animations is 'hardware acceleration.' The old way of animating with javascript is essentially a highly optimized hack. With CSS animations you have hardware support to do things that could never be done with the old hacks.

Case in point: http://scripty2.com/demos/cards/

15,902 Comments

@Alex,

Thanks for the insight regarding style-batching. I've never even heard of such a thing. I'll also have to take a look at the Scripty2 thing - other than having an adorable name, it sounds like some cool stuff.

For the time being, I don't necessarily mind doing the setTimeout(). My frustrating only ever really comes from not understanding; now that I get it, I don't mind. Plus, a 0ms (which probably is forced to the minimum of like 10ms) is hardly a deal breaker in performance.

74 Comments

Wait a minute! Scripty?? That seems to be some sort of library which is launching an attack on jQuery. Cute name indeed! Please don't make me have to keep track of another set of javascript files :( :( :(

74 Comments

@Alex,

The old 'here first' argument, eh? I noticed that it was part of Prototype. I know that jQuery is the infant in the room. It's just that my brain is hurting from all the new toys. Must. Step. Away.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel