CSS3 Animation With jQuery Fallbacks

Addy Osmani | July 6, 2011

In today's post, we'll be taking a look at how you can use CSS3 transitions to power your application's animations with jQuery's .animate() method as a fallback where transitions aren't supported. The benefit of transitions is that unlike JavaScript based animations, they're hardware accelerated in certain browsers and mobile devices, resulting in a potentially smoother overall animation than you would normally experience. 

In a lot of cases your code will  work seamlessly with the solutions presented today, so the effort involved in applying them is minimal. Before we get started, if you'd prefer to see a sample of demos using today's fallbacks before reading the post, feel free to check some out below. All demos are hosted on jsFiddle so you can easily fork and play around with them straight away.

Superman sample animation scene: Video Demo

Basic bouncing-ball concept: Video Demo

Interactive Demos

Demo 1

Demo 2

 

Demo 3

Demo 4

 

Introduction to CSS3 Transitions

For readers who haven't looked into transitions before, we're going to go through a quick overview of exactly what CSS3 transitions have to offer. Transitions are a part of the draft CSS3 specification and provide a means to animate changes in CSS properties rather than having those changes take effect instantaneously.

With transitions, the computed value of a property transitions over time from the old value to the new value. What this means is if a script queries the computed style of a property as it is transitioning, it will see an intermediate value that represents the current animated value of the property.

Let's take a look at an example of transitions in action: if we wanted to change CSS properties such as the margin, padding and height of an element on hover, normally this would happen instantaneously. With transitions however, the change can occur over an interval of time using a number of short-hand easing equations. Sample code for how to achieve this with CSS3 transitions for a miniature gallery can be found below:

.note {  
    padding:30px;  
    color:#000;  
    font-family: Arial, Helvetica, sans-serif;  
    text-align:center;  
    width:100px;  
    height:150px;  
    margin:10px;  
    float:left;  
    text-shadow:0px 1px 1px #fff;  
    background: #83C8F7;  
    background: -moz-linear-gradient(top, #83C8F7 0%, #207ce5 100%);  
    background: -webkit-gradient(linear, left top, left bottombottom, color-stop(0%,#83C8F7), color-stop(100%,#207ce5));  
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#83C8F7', endColorstr='#207ce5',GradientType=0 );  
    box-shadow:0px 7px 14px #666666;  
    border-top:1px solid #fff;  
}  
.photo{  
    border:1px solid #fff;  
    width:95px;  
    height:120px;  
    margin-top:10px;  
    background:url('https://addyosmani.com/blog/wp-content/uploads/2011/04/painting21.gif');  
    background-position:-100px -120px;  
}  
.photo:hover{  
    padding:5px;  
    height:150px;  
    margin:-25px 0px 0px -5px;  
    box-shadow:1px 4px 2px 0px #000;  
    background-position: -450px -120px;  
    /* apply transitions to all properties*/  
    -moz-transition: all 0.5s ease-out;  
    -o-transition: all 0.5s ease-out;  
    -webkit-transition: all 0.5s ease-out;  
    transition: all 0.5s ease-out;  
}


Demo: https://jsfiddle.net/addyosmani/WApWn/show/

For a complete list of CSS properties that are animatable see the W3C specs on transitions here: https://dev.w3.org/csswg/css3-transitions/. I also recommend reading https://samuli.hakoniemi.net/css3-transitions-are-we-there-yet/ for a look at a wide range of different CSS3 transition demonstrations.

CSS Transition support

Below we can see a list of the browsers that have been confirmed as supporting CSS3 transitions.

  • Google Chrome 9+
  • Opera 10.5+
  • Firefox 4+
  • Safari 3.2+

Although this list is promising, one of the main issues with implementing CSS3 transitions now is that support for them is not what could be described as fully cross-browser compatible. As they're a relatively recent feature, users who are still on older browsers (IE6+, Firefox 3 and below) won't be able to view them as animations and this can be challenging if you're aiming for a consistent user-experience. There are however solutions to this problem and we'll be taking a look at some of them in today's post.

Cross-browser Transitions with jQuery

If CSS3 transitions aren't going to be available, the next obvious language to fall back on is JavaScript. Using Modernizr, a popular feature detection tool, we can detect support for CSS3 transitions quite easily. We are then able to serve up JavaScript powered animations to browsers which don't support it.

Next, rather than handling all of the cross-browser challenges of smooth animation ourselves, we can make use of jQuery's .animate() features to provide the same animation effect if it can't be done in the current browser using CSS3. The only problem that then remains is getting .animate() to use CSS3 transitions if they're supported and easily fallback to jQuery if they're not. So what do we need to make this happen?

Our requirements:

  • Detect the vendor prefix needed for the current browser's version of transitions
  • (if they're supported) fallback to jQuery
  • if they're not attempt to support as much of the documented jQuery .animate() signature as possible (with respect to callbacks, duration, properties etc) within the scope of the post.

Note: Keep in mind that .animate() is considerably more complex than what I'll be showing you in the following example – this will support most basic animations, but really serves as a starting point for the solutions I'll be recommending later.

JavaScript: jquery.css3transitions.js

$(function(){

/*Note: you can replace getPrefix() with Modernizr._vendorPrefix in production. I'm showing an alternative implementation here just so that you can
see what's effectively being done under the hood*/
        function getPrefix( prop ){
            var prefixes = ['Moz','Webkit','Khtml','0','ms'],
                elem     = document.createElement('div'),
                upper      = prop.charAt(0).toUpperCase() + prop.slice(1),
                pref     = "";

            for(var len = prefixes.length; len--;){
                if((prefixes[len] + upper) in elem.style){
                    pref = (prefixes[len]);
             }
        }

        if(prop in elem.style){
            pref = (prop);
        }

        return '-' + pref.toLowerCase() + '-';

        }

        $.fn.extend({
            defaultAnimate: $.fn.animate,
            animate: function(props, speed, easing, callback) {
                        var options = speed && typeof speed === "object" ? jQuery.extend({}, speed) :{
                                complete: callback || !callback && easing ||
                                jQuery.isFunction( speed ) && speed,
                                duration: speed,
                                easing: callback && easing || easing && !jQuery.isFunction(easing) && easing
                            };   
                          return $(this).each(function() {
                            var $this = $(this),
                                altTransition,
                                easing = (options.easing) ? easing : 'ease-in-out',
                                prefix = (getPrefix('transition'));
                 
                                if (Modernizr.csstransitions) 
                                {
                                      $this.css(prefix + 'transition', 'all ' + speed / 1000 + 's ease-in-out').css(props);
                                      setTimeout(function() {
                                        $this.css(prefix + 'transition', altTransition);
                                        if ($.isFunction(options.complete)) {
                                             options.complete();
                                        }
                                      }, speed);
                                }
                                else{
                                     $this.defaultAnimate(props, options); 
                                }
                        })
                    }
                
        });

});

JavaScript: app.js

$(function(){  
        function updateStatus(msg){  
        $('.status').html(msg);  
    }  
        function compatTest(){  
        if (Modernizr.csstransitions) {  
            $('.compat span').html('yes');  
        }  
    }  
        compatTest();  
        $('.alt0').animate({width: '480px'}, 4000, function(){  
                updateStatus('Animation 1 Complete');  
        });  
        $('.alt1').animate({width: '480px'}, 5000, function(){  
                updateStatus('Animation 2 Complete');  
        });  
        $('.alt2').animate({width: '480px'}, 7000, function(){  
                updateStatus('Animation 3 Complete');  
        });  
});

HTML:

<!DOCTYPE html>  
<html>  
<head>  
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">  
  <title>Demo 1</title>  
  <link href='css/style.css' rel='stylesheet' type='text/css'>  
  <link href='https://fonts.googleapis.com/css?family=Dancing+Script' rel='stylesheet' type='text/css'>  
  <script type='text/javascript' src='js/jquery-1.5.1.min.js'></script>  
  <script type='text/javascript' src="js/modernizr-1.7.min.js"></script>  
  <script type='text/javascript' src="js/jquery.css3transitions.js"></script>  
  <script type='text/javascript' src="js/app.js"></script>  
</head>  
<body>  
  <div class="container">  
    <h1>CSS3 Transitions</h1>  
    <h4>with a jQuery fallback</h4>  
    <div class="bar"><div class="box alt0"></div></div>  
    <div class="bar"><div class="box alt1"></div></div>  
    <div class="bar"><div class="box alt2"></div></div>  
    <div class="notebar compat">CSS3 transitions supported? <span>no</span></div>  
    <div class="notebar">Using : sample code</div>  
    <div class="notebar status">Waiting for animations to complete...</div>  
</div>  
</body>  
</html>

Demo Preview:


Demo: https://jsfiddle.net/addyosmani/A89Sq/show

While this works fine for animating basic properties using the signature variants supported by .animate(), the routine above is in no way comprehensive for complete coverage of what it's fully capable of. If you're looking for something good enough for production use, I recommend reading on below for an introduction to two more complete implementations of transition fallbacks.

If you did however want to play around with the above could further, there are a number of areas in which it could be improved. It could include support for fx steps, fadeIn()/fadeOut(), CSS3 transitionend events, jQuery's queues and delay() as well as improve the interpretation of more complex CSS3 transitions and transforms.You could also easily add support for the shorthand animation speeds 'slow' and 'fast' through jQuery.fx.speeds.slow and jQuery.fx.speeds.fast if needed.

Additional Demos:

Adrian Sinclair, a fellow developer interested in transition fallbacks came up with this next demo which we'll be also testing out later with two other fallback solutions we'll be reviewing. Adrian's demo below uses a slightly different version of code than I presented above but it's aiming at the same general idea of transition fallback support through jQuery.


Demo: https://jsfiddle.net/addyosmani/Mxyaj/show/

More Robust Solutions

Rather than investing time and effort into extending the above code, I'd like to recommend two other solutions that are both more feature extensive, but also which get updated more regularly than the example I've shown you today.

jquery.transitions.js

The first solution I'd like to recommend is jQuery plugin developer Louis-Rémi Babé's jquery.transition.js. It's a robust jQuery fallback for CSS3 transitions and here's what Louis had to say about it when I spoke to him earlier:

"Unlike other transition polyfills, this plugin is not a monkey patch over jQuery animation mechanism. It is actually a patched version of effects.js (animation component), stripped from the redundant code to make it a lightweight plugin.*

*The main benefit is that the full jQuery API is available. Even better, it has been designed to be compatible with cssHooks. The first compatible one isjquery.transform.js. Others will follow.

jquery.transition.js has been tested against jQuery unit-tests, and only 8 of them fail, mostly for timing issues. It benefits from the experience of writingcsstransition.net to workaround implementation quirks.

In my opinion, its a good practice to test animations both with and without the plugin to see if the benefit experienced is real"

To summarize, let's now breakdown the benefits and downsides to using jquery.transitions.js:

Benefits

  • It supports the complete jQuery API, failing only 8 tests in the official jQuery test-suite
  • Due to test coverage, you're ensured a significantly more stable solution that will work with more complex animations
  • In addition to animate() it also supports show(), hide(), fadeTo(), toggle() and stop()
  • Supports jQuery queues.
  • Compatibility with jquery.transform.js

Downsides

  • Limited Opera compatibility – transitions in jquery.transitions.js work best with Mozilla and Webkit based browsers. The source claims the reason for this is the instability of Opera 11's transitions implementation. This is easily patchable however if you still want to enable CSS3 transitions rather than the fallback for Opera.
  • Transitions are disabled for special easing and step functions.

The minor downsides aside, in my experience Louis's solution is considerably more stable for heavy-duty animations than many of the other homebaked solutions out there. I definitely recommend checking it out.

jQuery transitions demos:

Demo 1 (Progress Bars): https://jsfiddle.net/addyosmani/XKrcX/show

Demo 2 (Box Grid): https://jsfiddle.net/addyosmani/fQhsv/show

Demo 3 (Sliding content): https://jsfiddle.net/addyosmani/55qRn/show/

Bonus demo:

As we've been taking a look at fairly simplistic animations so far, it's also useful to compare how well the transitions plugin handles actual animated scenes.

For this purpose, I've created a sample scene from 'All-Star Superman' which will similarly use CSS3 transitions if they're supported, but happily run using jQuery's standard .animate() if not. It's a short scene as this is just a demonstration, but you can easily see some of the benefits of maintaining the jQuery-stynax you're already used to for defining animations vs. perhaps opting for writing out all your transition statements separately.

var animScenes = animScenes || {};

animScenes = {
    city: $('#container'), 
    superman : $('#superman'), 
    dc : $('#dccomics'), 
    lex : $('#lexluthor'), 
    logo : $('#supermanlogo'),
    
    triggerScene1: function(){
       var sq = this;
       this.dc.animate({'opacity':'0'},3000, function(){
           sq.triggerScene2(); 
        });
    },
    
    triggerScene2 :function(){
       var sc =  this;
       this.dc.remove();
       //animate city for fake perspective
       this.city.animate({'top':'-200px'}, 5000);
       //superman flies into view
       this.superman.show().animate({'width':'436px','height':'326px'}, 3000);
       //lex flies close to catch up
       this.lex.show().animate({'marginTop':'90px', 'width':'190px','height':'190px'}, 5000, function(){
       //superman speeds away
           sc.superman.animate({'marginTop':'-400px','marginLeft':'1000px'}, 500, function(){
                //lex chases after
                sc.lex.animate({'marginTop':'-320px','marginLeft':'1000px'}, 300);
                //end scene with logo fading in
                sc.logo.show().animate({'width':'500px','height':'228px'}, 900);
            }); 
        });
    }        
};

animScenes.triggerScene1();

If you're looking for an .animate() solution that provides stable, tested CSS3 transition support, I strongly recommend using Louis's plugin. You can get the sources to it over on GitHub.

jQuery.animate-enhanced.js

The other solution you might like to check out is Ben Barnett's jQuery.animate-enhanced.js. Similar to Louis's, his plugin analyzes the properties you're animating on and selects the most appropriate approach for use in your current browser.

Benefits

  • Supports fadeIn(), .fadeOut(), .slideUp(), .slideDown() and slideToggle().
  • Supports $.fn.stop([clearQueue, jumpToEnd, leaveTransforms])
  • Includes $.translate() method to calculate x/y transforms from CSS3 Matrix
  • Animation 'queue' support (including .delay())
  • Supports Opera out of the box

Downsides

  • Works well with simple animations but can run into issues with more complex ones
  • Occasionally has issues with animation and positioning

I first played around with Ben's plugin about five months ago and I think the only downside to it is that it occasionally has trouble animating more complex animations which is something I haven't seen jquery.transitions.js affected by as much. It's still quite a strong contender for usage and I'd recommend reading up on it more before making a decision on which plugin is right for you.

Ben's plugin is almost identical to the jQuery animate() function, but unlike Louis's comes with three additional parameters, which are optional:

  • avoidTransforms: (bool) By default his plugin converts left and top animations to the CSS3 style -webkit-transform (or equivalent) to aid hardware acceleration. This functionality can be disabled by setting this to true.
  • useTranslate3d: (bool) By default the plugin uses 2d translations due to wider browser support. Setting this to true to use translate3d instead. This is recommended for iPhone/iPad development.
  • leaveTransforms: (bool) By default if the plugin is animating a left or a top property, the translate (2d or 3d depending on setting above) CSS3 transformation will be used. To preserve other layout dependencies, once the transition is complete, these transitions are removed and converted back to the real left and top values. You can set this to true to skip this functionality.

jQuery Animate-Enhanced demos:

Demo 1 (Progress Bars): https://jsfiddle.net/addyosmani/XSLbe/show

Demo 2 (Box Grid): https://jsfiddle.net/addyosmani/kfGUc/show

Demo 3 (Sliding content): https://jsfiddle.net/addyosmani/XwJHe/show/ (breaks the layout whilst jquery.transitions.js doesn't)

Conditional Polyfill Loading Approach

Another approach to handling the requirement for a fallback is by using a conditional polyfil loader like yepnope (if you're interested in a tutorial on yepnope, I wrote one for .net magazine back in issue #213) . With the help of Modernizr, you could easily test for CSS3 transition support in the script loader phase and then load a pure CSS3 version of the animation using a stylesheet if transition support is detected. If not, you could then load a jQuery + fallback script version of the animation.

yepnope({  
  test: Modernizr.csstransitions,  
  yep: 'animation.css',  
  nope: ['base.css', 'jquery.transition.js', 'animation.js'],  
  complete: function () {  
    alert('Everything has completed loading!');  
  }  
});

The reason I personally prefer not doing this is because it requires you writing two versions of the same animation (one in CSS, another in JS/jQuery) whilst simply defining your animation through jQuery's .animate() (and having a plugin convert to transitions for you) can be easier depending on the complexity of what you're working with. Use your best judgement with respect to how you structure your solution, but you should now have at least a general idea of at least two ways this problem could be tackled. The above works best if your animation code is not likely to be complex.

Conclusions & Important Notes

And that's it!. If you decide to use one of the fallbacks presented in today's post, remember to thoroughly test your animations correctly render throughout without any issues. Also remember that using transitions don't always mean smoother solutions (in particular when they're triggered from the DOM rather than through CSS). It's just as important to test your pages both with and without transition plugins to ascertain what the real perceived improvements are both on desktop and mobile devices.

I hope this comes in useful!.

Update: Paul Irish tells me that Modernizr will soon be implementing Modernizr._vendorPrefix to expose the vendor prefix required for things like CSS3 transitions. Once available, it will no longer be required for you to have your own custom function to supply this info. Woot!

Addy has kindly granted permission for this article to be republished here on Script Junkie. Please visit Addy's site to see more great posts.

Conclusions & Important Notes

And that's it!. If you decide to use one of the fallbacks presented in today's post, remember to thoroughly test your animations correctly render throughout without any issues. Also remember that using transitions don't always mean smoother solutions (in particular when they're triggered from the DOM rather than through CSS). It's just as important to test your pages both with and without transition plugins to ascertain what the real perceived improvements are both on desktop and mobile devices.

 

About the Author

Addy Osmani is a User Interface Developer from London, England. An avid blogger, he has a passion for encouraging best practices in client-side development, in particular with respect to JavaScript and jQuery. He enjoys evangelizing the latter of these and is on the jQuery Bug triage, API and front-end teams. Addy works at AOL where he's a JavaScript developer on their European Products & Innovation team.

Find Addy on: