Detect if property is animatable by CSS3 transition?

Problem

The list of properties that can be animated with a CSS3 transition is not consistent among browsers and is a subject to change with new browser versions. For example, -moz-transform is not animatable with -moz-transition in FF3.6 but it is in FF4.

So, is there a way to detect in JavaScript if a particular property is animatable? I wouldn't like to use user agent sniffing as it's not reliable.

Thanks in advance!

Problem courtesy of: Mourner

Solution

Edit: see Jordan's answer for a good technique on detecting animatable properties.

I'm afraid there is no straightforward way to detect if a property is animatable. However, the properties are consistent for the most part (the only problem I've encountered is with FF4 transition + text shadow + transform).

http://www.w3.org/TR/css3-transitions/#the-transition-property-property-#properties-from-css-

Firefox 3.6 doesn't support css transitions, you can detect this with a js library such as Modernizr:

http://www.modernizr.com/

Solution courtesy of: Duopixel

Discussion

Yes, there is a way. Demonstration follows, explanation below. There are some very important caveats involved, so make sure you read on.

The following code will test if the browser can animate between two values.

The code

jsFiddle demo.

/*
@param property  The property to test.
@param from      A valid starting value for the animation.
@param to        A valid ending value for the animation.
@param [element] The element to test with. (Required for testing
                 properties with prerequisites, e.g. "top" requires
                 non-static position.)
*/
function isAnimationSupported(property, from, to, element) {
    var doc = document.documentElement,
        style = doc.appendChild(document.createElement("style")),
        rule = [
                'capTest{',
                    '0%{',   property, ':', from, '}',
                    '100%{', property, ':', to,   '}',
                '}'
               ].join(''),
        propCamel = property.toCamelCase(),
        prefixes = 'moz ms o webkit '.split(' '), // Unprefixed last, see comments.
        prefixCount = prefixes.length,
        canAnimate = false;

    element = doc.appendChild((element)
            ? element.cloneNode(false)
            : document.createElement('div'));

    // Detect invalid start value. (Webkit tries to use default.)
    element.style[propCamel] = to;

    // Iterate through supported prefixes.
    for (var i = 0; i < prefixCount; i++) {

        // Variations on current prefix.
        var prefix  = prefixes[i],
            hPrefix = (prefix) ? '-' + prefix + '-' : '',
            uPrefix = (prefix) ? prefix.toUpperCase() + '_' : '';

        // Test for support.
        if (CSSRule[uPrefix + 'KEYFRAMES_RULE']) {

            // Rule supported; add keyframe rule to test stylesheet.
            style.sheet.insertRule('@'+ hPrefix + 'keyframes ' + rule, 0);

            // Apply animation.
            var animationProp = (hPrefix + 'animation').toCamelCase();
            element.style[animationProp] = 'capTest 1s 0s both';

            // Get initial computed style.
            var before = getComputedStyle(element)[propCamel];

            // Skip to last frame of animation.
            // BUG: Firefox doesn't support reverse or update node style while
            // attached.
            doc.removeChild(element);
            element.style[animationProp] = 'capTest 1s -1s alternate both';
            doc.appendChild(element);
            // BUG: Webkit doesn't update style when animation skipped ahead.
            element.style[animationProp] = 'capTest 1s 0 reverse both';

            // Get final computed style.
            var after = getComputedStyle(element)[propCamel];

            // If before and after are different, property and values are animable.
            canAnimate = before !== after;
            break;
        }
    }

    // Clean up the test elements.
    doc.removeChild(element);
    doc.removeChild(style);

    return canAnimate;
}

// Cribbed from Lea Verou's prefixfree.
String.prototype.toCamelCase = function() {
    return this.replace(/-([a-z])/g, function($0, $1) { return $1.toUpperCase(); })
               .replace('-','');
};

How to use

The mandatory arguments for this are the property to animate and the starting and finishing values it should take. Optionally, you can pass an element with other initial styles set, e.g. position: absolute. (The function clones the element, so you can pass nodes from the document and they won't be changed.) If you don't pass any element, the animation is tested on a div with whatever default styles the UA applies.

How it works

A keyframe animation rule is added to a dummy stylesheet, with the initial frame set to the from value and the final frame set to the to value. This animation is applied to an element. We then inspect the computed style for the animated property to see if it is different when the animation starts from the initial frame compared to when it starts from the final frame.

The reason this works is because the animable properties for both transitions and keyframe animations are the same, and the browser will only apply keyframe values if the property supports animation.

Caveats (read before using, some of these are nasty!)

There are several inconsistencies in how browsers handle animations. A couple of these I have worked around in as future-proof a way as possible; however, a few of them are intractable.

Most notably, Firefox tweens position values (e.g. left) on static elements while others (e.g. Webkit and Opera) do not. It doesn't actually move the element, but the value of that property is updated. Thus, you will get different results between browsers if you try to animate a position value without passing a non-statically positioned element.

The most current versions of major browsers that support CSS transitions also support CSS keyframes, although some older versions support the former but not the latter. (E.g. Opera 11.)

Finally, if I were doing this more elegantly I would use prefixfree to determine the correct prefix to use directly; currently I test against an array of prefixes, starting with the unprefixed version.

Discussion courtesy of: Jordan Gray

This recipe can be found in it's original form on Stack Over Flow.