How to write maintainable and efficient CSS3 animation effects

09 Oct 2012 by David Corvoysier

CSS3 animation effects are now available in most recent browsers, and with decent performances thanks to hardware acceleration: now is a good time to start adding those fancy visual effects you've ever been dreaming of to your web site ...

CSS3 animation effects power comes at the cost of an ever-increased complexity, and using them carelessly will produce a code not only very difficult to read, but also nearly impossible to maintain.

In this article, I try to identify a few pitfalls you should avoid.

Use CSS static rules whenever it is possible

This is a general good coding practice to avoid inline CSS and Javascript to produce maintainable code, but it is even more important when dealing with very complex properties like those involved in most CSS3 graphic features.

Below is an example of a rotation effect implemented using inline CSS and Javascript:

<div onmouseover="this.style.transform='rotate(180deg)';
    this.style.transition='all 4s cubic-bezier(1.000, -0.530, 0.405, 1.425)';"
    onmouseout="this.style.transform='';this.style.transition=''">
    Bad Practice
</div>

The same effect can also be achieved using a single static CSS rule.

.rotating:hover {
transition: all 4s cubic-bezier(1.000, -0.530, 0.405, 1.425);
transform: rotate(180deg);
}

<div class='rotating'>
    Good Practice
</div>

You can test below the result (hover the elements to trigger the effect)

​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​

Try to address dynamic styling through fine-grained properties

There are use cases where it is difficult to rely on static CSS rules only, for instance when the style of an element depends on specific local conditions (most of time: user-input)

In those cases, try nevertheless to keep all properties that won't change at runtime in static CSS rules, and use direct style assignments only for dynamic properties.

The example below applies a CSS translation and a CSS rotation to the image of a ball so that it looks like it is rolling towards the point where the user has clicked.

The static style properties of the image element are assigned using a CSS rule.

img {
    position: absolute;
    bottom: 0px;
    left: 0px;
    width: 50px;
    height: 50px;    
    transition: all 1s ease-out;
}

And the dynamic translation and rotation are applied dynamically in an event handler using a direct style assignment through javascript:

function move(event) {
    // Calculate the left offset based on the container absolute position
    var offset = container.getBoundingClientRect().left;
    // Calculate the distance to align the center of the ball to the mouse 
    var d = event.clientX -offset -25;
    ball.style.left=d+'px';
    // The rotation equals the length of the arc divided by the ball radius
    ball.style['transform']='rotate('+d/25+'rad)';
}

You can test below the result (click in the element to attract the ball)

Organize your content for animation

You may find this tip really obvious, but since I have seen this mistake made already, it is worth mentioning it anyway: if you have to apply the same transformation to a group of elements, group them together inside a containing element, and move the container instead of the individual containees.

The example below is a very simple slider.

Here is the page structure: we use an ul element to slide a group of elements stored in li elements.

<ul>
    <li onclick="slideLeft()">&lt;</li>
    <li>
        <ul id="slider">
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
            <li>5</li>
            <li>6</li>
            <li>7</li>
            <li>8</li>
        </ul>
    </li>
    <li onclick="slideRight()">&gt;</li>
</ul>

The CSS rules to display everything inline (note also the overflow hidden to hide the parts that will slide)

li {
    width: 30px;
    font-size:30px;
    text-align: center;
    display: inline-block;
    margin: 0px;
    padding: 0px;
}
ul {
    margin: 0px;
    padding: 0px;
    white-space:nowrap;
}
ul > li {
    list-style: none;
    position: relative;
    overflow: hidden;
    border-radius: 15%;
    background-color: lightgrey;
}
ul > li > ul {
    position: relative;
    transition: left 0.5s ease-out;
}

And finally the javascript to animate it:

function slideLeft() {
    index--;
    index = Math.max(index,0);
    setSlider(index);
}
function slideRight() {
    index++;
    index = Math.min(index,7);
    setSlider(index);
}
function setSlider(index) {
    slider.style.left=-index*38+'px';
}
var slider = document.getElementById("slider");
var index = 0;

You can test below the result (hover the element to trigger the effect)

  • <
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  • >

Take advantage of dynamic CSS rule insertion

As mentioned earlier, not all CSS properties can be assigned statically, but dynamic style assignments through Javascript is not the only option available: dynamic CSS rule insertion at runtime can sometimes provide an alternative solution.

In a nutshell, DOM Level 2's insertRule allows new CSS rules to be inserted dynamically in a document's stylesheet at runtime: so, instead of directly setting the style of an element, you can insert a new CSS rule with a selector that matches that element.

The example below applies a specific CSS transformation to children elements of a container based on their index.

The common style properties of the child elements are assigned using a static CSS rule:

.ball {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
margin: auto;
width: 6px;
height: 6px;
border-radius: 50%;
background-color: red;
}

The specific style properties for each elements are defined using dynamic CSS rules:

.ball:nth-child(12) {
transform: rotateZ(86deg) translateX(3px);
}

The dynamic CSS rules are inserted at runtime through Javascript:

function getBallTransform(index){
    var transform = "rotateZ("+Math.round(index*1440/MAX_BALLS)+"deg)";
    transform += " translateX("+Math.round(index*50/MAX_BALLS)+"px)";
    return transform;
}
function setBallRules(n) {
    if( document.styleSheets.length == 0 ) {
        var style = document.createElement('style');
        style.type = "text/css";
        document.getElementsByTagName('head')[0].appendChild(style);
    }
    styleSheet = document.styleSheets[document.styleSheets.length-1];
    for (var i=0;i<n;i++) {
        var rule = '.ball:nth-child(' + i + ') {';
        rule += 'transform:' + getBallTransform(i) + '}';
        styleSheet.insertRule(rule,styleSheet.cssRules.length);
    }
}
...
setBallRules(MAX_BALLS);

Note: as an alternative, these rules could be generated server-side based on the expected number of balls.

When a new ball is inserted, it is simply assigned a className allowing the CSS rules to match.

function addBall() {
    var container = document.getElementById('example2');
    var balls = container.getElementsByTagName('div');
    if(balls.length<=MAX_BALLS){
        var ball = document.createElement('div');
        ball.className = 'ball';
        container.appendChild(ball);
    }
}

You can test below the result (hover the element to trigger the effect)

comments powered by Disqus