Controlling CSS Animations and Transitions with JavaScript

The following is a guest post by Zach Saucier. Zach wrote to me telling me that, as a frequenter on coding forums like Stack Overflow, he sees the questions come up all the time about controlling CSS animations with JavaScript, and proved it with a bunch of links. I've had this on my list to write about for way too long, so I was happy to let Zach dig into it and write up this comprehensive tutorial.

Web designers sometimes believe that animating in CSS is more difficult than animating in JavaScript. While CSS animation does have some limitations, most of the time it's more capable than we give it credit for! Not to mention, typically more performant.

Coupled with a touch of JavaScript, CSS animations and transitions are able to accomplish hardware-accelerated animations and interactions more efficiently than most JavaScript libraries.

Let's jump straight in!

Quick Note: Animations and Transitions are Different

Transitions in CSS are applied to an element and specify that when a property changes it should do so over gradually over over a period of time. Animations are different. When applied, they just run and do their thing. They offer more fine-grained control as you can control different stops of the animations.

In this article, we will cover each of them separately.

Manipulating CSS Transitions

There are countless questions on coding forums related to triggering and pausing an element's transition. The solution is actually quite simple using JavaScript.

To trigger an element's transition, toggle a class name on that element that triggers it.

To pause an element's transition, use getComputedStyle and getPropertyValue at the point in the transition you want to pause it. Then set those CSS properties of that element equal to those values you just got.

Note we're changing background-size this time. There are many different CSS properties that can be transitioned or animated, typically one that have numeric or color values. Rodney Rehm also wrote a particularly helpful and informational article on CSS transitions which can be found here.

Using CSS "Callback Functions"

Some of the most useful yet little-known JavaScript tricks for manipulating CSS transitions and animations are the DOM events they fire. Like: animationend, animationstart, and animationiteration for animations and transitionend for transitions. You might guess what they do. These animation events fire when the animation on an element ends, starts, or completes one iteration, respectively.

These events need to be vendor prefixed at this time, so in this demo, we use a function developed by Craig Buckler called PrefixedEvent, which has the parameters element, type, and callback to help make these events cross-browser. Here is his useful article on capturing CSS animations with JavaScript. And here is another one determining which animation (name) the event is firing for.

The idea in this demo is to enlarge the heart and stop the animation when it is hovered over.

The pure CSS version is jumpy. Unless you hover over it at the perfect time, it will jump to a particular state before enlarging to the final hovered state. The JavaScript version is much smoother. It removes the jump by letting the animation complete before applying the new state.

Manipulating CSS Animations

Like we just learned, we can watch elements and react to animation-related events: animationStart, animationIteration, and animationEnd. But what happens if you want to change the CSS animation mid-animation? This requires a bit of trickery!

The animation-play-state Property

The animation-play-state property of CSS is incredibly helpful when you simply need to pause an animation and potentially continue it later. You can change that CSS through JavaScript like this (mind your prefixes):

However, when a CSS animation is paused using animation-play-state, the element is prevented from transforming the same way it is when an animation is running. You can't pause it, transform it, resume it, and expect it to run fluidly from the new transformed state. In order to do that, we have to get a bit more involved.

Obtaining the Current Keyvalue Percentage

Unfortunately, at this time, there is no way to get the exact current "percentage completed" of a CSS keyframe animation. The best method to approximate it is using a setInterval function that iterates 100 times during the animation, which is essentially: the animation duration in ms / 100. For example, if the animation is 4 seconds long, then the setInterval needs to run every 40 milliseconds (4000/100).

This approach is far from ideal because the function actually runs less frequently than every 40 milliseconds. I find that setting it to 39 milliseconds is more accurate, but relying on that is bad practice, as it likely varies by browser and is not a perfect fit on any browser.

Obtaining the Animation's Current CSS Property Values

In a perfect world, we would be able to select an element that's using a CSS animation, remove that animation, and give it a new one. It would then begin the new animation, starting from its current state. We don't live in that perfect world, so it's a bit more complex.

Below we have a demo to test a technique of obtaining and changing a CSS animation "mid stream", as it were. The animation moves an element in a circular path with the starting position being at the top center ("twelve o'clock", if you prefer) When the button is clicked, it should change the starting position of the animation to the element's current location. It travels the same path, only now "begins" at the location it was at when you pressed the button. This change of origin, and therefore change of animation, is indicated by changing the element's color to red in the first keyframe.

We need to get pretty deep to get this done! We're going to have to dig into the stylesheet itself to find the original animation.

You can access the stylesheets associated with a page by using document.styleSheets and iterate through it using a for loop. The following is how you can use JavaScript to find a particular animation's values in a CSSKeyFrameRules object:

Once we call the function above (e.g. var keyframes = findKeyframesRule(anim)), you can get the get the animation length of the object (the total number of how many keyframes there are in that animation) by using keyframes.cssRules.length. Then we need to strip the "%" from each of the keyframes so they are just numbers and JavaScript can use them as numbers. To do this, we use the following, which uses JavaScript's .map method.

At this point, keys will be an array of all of the animation's keyframes in numerical format.

Changing the Actual Animation (finally!)

In the case of our circular animation demo, we need two variables: one to track how many degrees the circle had traveled since its most recent start location, and another to track how many degrees it had traveled since the original start location. We can change the first variable using our setInterval function (using time elapsed and degrees in a circle). Then we can use the following code to update the second variable when the button is clicked.

Next, we need to change the % into a degree of the circle. We can do this by simply multiplying the new first percentage by 3.6 (because 10 0* 3.6 = 360).

Finally, we create the new rules based on the variables obtained above. The 45-degree difference between each rule is because we have 8 different keyframes that go around the circle. 360 (degrees in a circle) divided by 8 is 45.

Then we reset the current percent setInterval so it can be run again. Note the above is WebKit prefixed. To make it more cross-browser compatible you could possibly do some UA sniffing to guess which prefixes would be needed:

Turning Animations into Transitions

As we've seen, manipulating CSS transitions can be simplified using JavaScript. If you don't end up getting the results you want with CSS animations, you can try making it into a transition instead and working with it that way. They are of about the same difficulty to code, but they may be more easily set and edited.

The biggest problem in turning CSS animations into transitions is when we turn animation-iteration into the equivalent transition command. Transition has no direct equivalent, which is why they are different things in the first place.

Relating this to our rotation demo, a little trick is to multiply both the transition-duration and the rotation by x. Then you need to have/apply a class to trigger the animation, because if you applies the changed properties directly to the element, well, there won't be much of a transition to be had. To kick off the transition (fake animation), you apply the class to the element.

Resetting CSS Animations

The trick to do this the correct way can be found here on CSS Tricks. The trick is essentially (if possible) remove the class that started the animation, trigger reflow on it somehow, then apply the class again. If all else fails, rip the element off the page and put it back again.

Use Your Head

Before starting to code, thinking about and planning how a transition or animation should run is the best way to minimize your problems and get the effect you desire. Even better than Googling for solutions later! The techniques and tricks overviewed in this article may not always be the best way to create the animation your project calls for.

Here's a little example of where getting clever with HTML and CSS alone can solve a problem where you might have thought to go to JavaScript.

Say we want a graphic to rotate continuously and then switch rotational direction when hovered. Learning what was covered in this article, you might want to jump in and use an animationIteration event to change the animation. However, a more efficient and better-performing solution can be found using CSS and an added container element.

The trick would be to have the spiral rotate at x speed in one direction and, when hovered, make the parent element rotate at 2x speed in the opposite direction (starting at the same position). The two rotations working against each other creates a net effect of the spiral rotating the opposite direction.

Related

Comments

Well sure, there’s a plugin out there for everything, but this article is not only about learning this technique but thinking about it and maybe using it in different ways in different places. I mean, yeah, you could just use that plugin you’ve pointed out, but the article is about learning. I didn’t actually know about everything in this article so it was pretty eye opening.

Great article that’s transparent about the complexities involved with this stuff. Thanks for sharing. Just a few things to point out…

That one example wasn’t a true pause/resume because easing isn’t maintained (it restarts each time).
You cannot jump to a specific spot in the css animation/transition like seek(). So no scrubbing. Correct me if I’m wrong.
It’s impossible (or incredibly complex) to independently control aspects of the transform like scale, rotation, skew, and position with different timing and/or easing. Imagine rotating for 5 seconds, but halfway through that, start scaling up, and then move for the final second using a unique ease. This is a major problem (in my opinion).
Easing options are pretty limited in css. You can’t do Elastic, Bounce, etc.

In my experience, simple animation is fine with CSS (assuming compatibility with older browsers isn’t a concern), but anything moderately complex quickly becomes a nightmare. There’s a reason specialized libraries like GSAP (as felix mentioned) are becoming so popular. For all the hype surrounding CSS animations/transitions, they just aren’t well-suited for a robust animation workflow. Few people are talking candidly about the limitations and complexities, so I sure appreciate your article.

I know your goal wasn’t to say JS animation is “bad” or CSS is “better” – I just thought I’d chime in with a few caveats I’ve stumbled across when wrestling with css animations . I don’t mean any of this in an argumentative way.

I’m all for using added libraries to get exactly what one wants! I’m not satisfied with the limitations of manipulating CSS animations at this point either, but I thought I’d share some ways to make it easier and more viable than most people know it to be.

You are correct in saying it isn’t a true pause, I knew that while making it. The easing does reset, but without a library it is about as good as we can get without creating a whole new animation like I did with the rotating circle example each pause. I don’t think the purpose of CSS animations is to be a robust resource for animations to be used in the place of a library, but I do think that a library is not always needed and believe that the article contains some useful tips in preventing the need for an added library in some cases.

I think what Zach has managed to do with css is commendable given the extremely restrictive nature of css animations and transitions.

As a motion graphics animator and front end web developer these articles always make me cringe though. The lengths that people have to go to make extremely basic animations work, and even then in the end most of them have issues, makes them unusable in a lot of situations.

The fact that an animator needs to understand (relatively) advanced javascript to make a circle go round, and glitchy at that, is a sad state of affairs for the web.

p.s. I’m an avid Greensock user, started with AS2/AS3 and now use the js version for complicated web animation, I can’t recommend it enough.

Actually, no, cubic-bezier() in css does NOT allow you do things like bounce or elastic easing, or many others that are available in JS and GSAP because cubic-bezier() only permits two control points. It does handle the basic easing functions well though.

I added some browser prefixes to the “callback functions” example. Could you try it now and see if it works? And I think your problems may be due to the embedding, could you also try it on CodePen directly and see if your problems still persist?

I think it may be safe to drop the “ms”, “moz,” and “o” prefixes for the animation callback event names these days. I’ve noticed that (desktop) Opera is using the webkit event name, while IE10 and Firefox are using the unprefixed event names.

Also you can use the Modernizr prefixed to add the correct prefixed event name for each browser. Which is really useful since you probably be using it to detect whether the browser supports CSS3 animations and transitions.

its simply amazing how most developers like to complicate things. much of what i seen here is easier to do with just the css3 . being a really good programmer is about making the simplest solution, not the fanciest or the most complicated.

I agree with simplicity! It’s all about simplicity- and the other main factor is PERFORMANCE (especially when we’re talking mobile). With CSS3 transitions, performance really shines since it’s “hardware accelerated” as seen here: