Smooth animations with CSS will-change

In user interfaces it is very common to apply animation effects in response to user input. Some of the most common effects involving fading, sliding or zooming UI components in or out to redirect the user’s focus. In HTML/CSS this is typically achieved by modifying the opacity and transform CSS properties, by using CSS transitions, CSS animations or Javascript. What you may not realise is that setting these properties can have unexpected effects on the rendering order, and even the layout of HTML elements.

Let’s look at opacity as a first example. Imagine we have some HTML like the following:

<div id="target">
  <div class="red square"></div>
<div class="blue square"></div>

with the following CSS applied:

.square { width: 200px; height: 200px; border: 10px solid black;
          position: absolute; color: white; padding: 10px; }
.red { background: red; top: 50px; left: 150px; z-index: 2; }
.blue { background: blue; top: 120px; left: 50px; z-index: 1; }
#target { margin: 20px 100px; }

You can see that red square has a z-index of 2, and the blue square has a z-index of 1, so the red square appears on top of the blue square as expected.

If we now apply an opacity property to the <div> with an id of “target” we might expect that the only change is to make the red square partially transparent. In fact the blue square now appears on top of the red square, as shown in the image below.

CSS opacity issues

The reason is that setting an opacity of less than 1 causes a stacking context to be created, so that now the red square and the blue square are no longer in the same stacking context, and their z-indexes don’t get compared.

An even more surprising effect can be seen if we set a transform property on the target <div>. If we set a transform of translate(0,0), we might expect nothing to happen, but instead the red square jumps off to the right, and again the blue square is on top of the red square.

CSS transform issues

When adding a transform property to an HTML element it not only creates a stacking context just as with opacity, it also begins acting as a container for any elements with position property value of absolute. Now the top and left offsets apply relative to the target <div>, instead of the whole page as previously.

If you’ve ever come across this behaviour, the chances are it was accidental, and probably unwanted. However even when there are no unexpected visual effects when setting opacity or transform the browser still needs to do work to reorganise the way elements are rendered, and perform extra layout due to this. This is not ideal when trying to achieve smooth animations of these properties, and could result in a glitch or jerk at the beginning or end of an animation.

In browsers other than Flow, there are other related problems, since applying an animation on opacity or transform can cause accelerated compositing mode to kick in, where bitmap image layers are allocated for the areas of the screen which are changed. Switching into and out of this mode can also cause glitches at the start or end of the animation as allocating these layer resources can be a heavy operation. This doesn’t apply to Flow as everything is rendered on the GPU so cached layers are not required.

In the past, content developers have tried to work around glitches caused by these issues by setting property values such as opacity: 0.999, or transform: translate3d(0,0,0), with translate3d typically being used in an attempt to force the browser to use GPU memory for its accelerated compositing layers. This will avoid the animation glitches, but it is a bad idea since it may well take up extra resources when the element is not being animated. This is especially important when targeting embedded devices with limited memory available. It also makes assumptions about how browsers are implemented, for example, that using translate3d(0,0,0) will cause GPU accelerated compositing mode to kick in.

The correct solution for avoiding glitches is to use the CSS will-change property, which is supported by all modern browsers. This lets the browser know that the element will be animated. For example, setting a value of will-change: transition, opacity will cause the element to be rendered as if it had non-default values for those properties (ie. with their own stacking contexts etc). Some browsers may also choose to allocate extra resources to make the animation smooth, eg. allocating accelerated compositing layers, so it must be used only where needed. Flow also supports CSS will-change, but since Flow does not need accelerated compositing to deliver high frame rate animations, using it will not result in the allocation of any extra layers.

If you’d like to automatically receive our posts by email please join our mailing list.