The following is a short extract from Tiffany’s new book, CSS Master, 2nd Edition.
Think of CSS animation as the more sophisticated sister to CSS transitions. Animations differ from transitions in a few key ways:
- Animations don’t degrade gracefully. If there’s no support from the browser, the user is out of luck. The alternative is to use JavaScript.
- Animations can repeat, and repeat infinitely. Transitions are always finite.
- Animations use keyframes, which offer the ability to create more complex and nuanced effects.
- Animations can be paused in the middle of the play cycle.
The latest versions of all major browsers support CSS animations. Firefox versions 15 and earlier require a -moz-
prefix; later version don’t. Internet Explorer versions 10 and 11 also support animations without a prefix, as do all versions of Microsoft Edge.
We can check for CSS animations support in a few ways. The first is by testing for the presence of CSSKeyframeRule
as a method of the window
object:
const hasAnimations = 'CSSKeyframeRule' in window;
If the browser supports the @supports
rule and CSS.supports()
API, we can use that instead:
const hasAnimations = CSS.supports('animation-duration: 2s');
As with transitions, we can only animate interpolatable values such as color values, lengths, and percentages.
Creating Your First Animation
We first have to define an animation using an @keyframes
rule. The @keyframes
rule has two purposes:
- setting the name of our animation
- grouping our keyframe rules
Let’s create an animation named pulse
:
@keyframes pulse {
}
Our keyframes will be defined within this block. In animation, a keyframe is a point at which the action changes. With CSS animations specifically, keyframe rules are used to set property values at particular points in the animation cycle. Values that fall between the values in a keyframe rule are interpolated.
At the minimum, an animation requires two keyframes: a from
keyframe, which is the starting state for our animation, and a to
frame, which is its end state. Within each individual keyframe block, we can define which properties to animate:
@keyframes pulse {
from {
transform: scale(0.5);
opacity: .8;
}
to {
transform: scale(1);
opacity: 1;
}
}
This code will scale our object from half its size to its full size, and change the opacity from 80% to 100%.
The keyframes
rule only defines an animation, though. By itself, it doesn’t make elements move. We need to apply it. Let’s also define a pulse
class that we can use to add this animation to any element:
.pulse {
animation: pulse 500ms;
}
Here, we’ve used the animation
shorthand property to set the animation name and duration. In order for an animation to play, we need the name of an @keyframes
rule (in this case, pulse
) and a duration. Other properties are optional.
The order of properties for animation
is similar to that of transition
. The first value that can be parsed becomes the value of animation-duration
. The second value becomes the value for animation-delay
. Words that aren’t CSS-wide keywords or animation property keyword values are assumed to be @keyframe
ruleset names.
As with transition
, animation
also accepts an animation list. The animation list is a comma-separated list of values. We could, for example, split our pulse animation into two rules—pulse
and fade
:
@keyframes pulse {
from {
transform: scale(0.5);
}
to {
transform: scale(1);
}
}
@keyframes fade {
from {
opacity: .5;
}
to {
opacity: 1;
}
}
Then we could combine them as part of a single animation:
.pulse-and-fade {
animation: pulse 500ms, fade 500ms;
}
Animation Properties
Though using the animation
property is shorter, sometimes longhand properties are clearer. Longhand animation properties are listed below:
Property | Description | Initial value |
---|---|---|
animation-delay |
How long to wait before executing the animation | 0s (executes immediately) |
animation-duration |
How long the cycle of an animation should last | 0s (no animation occurs) |
animation-name |
The name of an @keyframes rule |
none |
animation-timing-function |
How to calculate the values between the start and end states | ease |
animation-iteration-count |
How many times to repeat the animation | 1 |
animation-direction |
Whether or not the animation should ever play in reverse | normal (no reverse) |
animation-play-state |
Whether the animation is running or paused | running |
animation-fill-mode |
Specifies what property values are applied when the animation isn’t running | none |
The animation-delay
and animation-duration
properties function like transition-delay
and transition-duration
. Both accept time units as a value, either in seconds (s
) or milliseconds (ms
). Negative time values are valid for animation-delay
, but not animation-duration
.
Let’s rewrite our .pulse
ruleset using longhand properties. Doing so gives us the following:
.pulse {
animation-name: pulse;
animation-duration: 500ms;
}
The animation-name
property is fairly straightforward. Its value can be either none
or the name of the @keyframes
rule. Animation names have few restrictions. CSS keywords such as initial
, inherit
, default
, and none
are forbidden. Most punctuation characters won’t work, while letters, underscores, digits, and emojis (and other Unicode) characters usually will. For clarity and maintainability, it’s a good idea to give your animations descriptive names, and avoid using CSS properties or emojis as names.
To Loop or Not to Loop: The animation-iteration-count
Property
If you’re following along with your own code, you’ll notice that this animation only happens once. We want our animation to repeat. For that, we’ll need the animation-iteration-count
property.
The animation-iteration-count
property accepts most numeric values. Whole numbers and decimal numbers are valid values. With decimal numbers, however, the animation will stop partway through the last animation cycle, ending in the to
state. Negative animation-iteration-count
values are treated the same as 1
.
To make an animation run indefinitely, use the infinite
keyword. The animation will play an infinite number of times. Of course, infinite
really means until the document is unloaded, the browser window closes, the animation styles are removed, or the device shuts down. Let’s make our animation infinite:
.pulse {
animation-name: pulse;
animation-duration: 500ms;
animation-iteration-count: infinite;
}
Or, using the animation
shorthand property:
.pulse {
animation: pulse 500ms infinite;
}
Playing Animations: the animation-direction
Property
There’s still a problem with our animation, however. It doesn’t so much pulse as repeat our scaling-up animation. What we want is for this element to scale up and down. Enter the animation-direction
property.
The animation-direction
property accepts one of four values:
normal
: the initial value, playing the animation as specifiedreverse
: flips thefrom
andto
states and plays the animation in reversealternate
: plays even-numbered animation cycles in reversealternate-reverse
: plays odd-numbered animation cycles in reverse
To continue with our current example, reverse
would scale down our object by a factor of 0.5. Using alternate
would scale our object up for the odd-numbered cycles and down for the even-numbered. Conversely, using alternate-reverse
would scale our object down for the odd-numbered cycles and up for the even ones. Since this is the effect we want, we’ll set our animation-direction
property to alternate-reverse
:
.pulse {
animation-name: pulse;
animation-duration: 500ms;
animation-iteration-count: infinite;
animation-direction: alternate-reverse;
}
Or, using the shorthand property:
.pulse {
animation: pulse 500ms infinite alternate-reverse;
}
Using Percentage Keyframes
Our previous example was a simple pulse animation. We can create more complex animation sequences using percentage keyframes. Rather than using from
and to
, percentage keyframes indicate specific points of change over the course of the animation. Below is an example using an animation named wiggle
:
@keyframes wiggle {
25% {
transform: scale(.5) skewX(-5deg) rotate(-5deg);
}
50% {
transform: skewY(5deg) rotate(5deg);
}
75% {
transform: skewX(-5deg) rotate(-5deg) scale(1.5);
}
100% {
transform: scale(1.5);
}
}
We’ve used increments of 25% here, but these keyframes could be 5%, 10%, or 33.2%. As the animation plays, the browser will interpolate the values between each state. As with our previous example, we can assign it to a selector:
/* Our animation will play once */
.wiggle {
animation-name: wiggle;
animation-duration: 500ms;
}
Or using the animation
shorthand property:
.wiggle {
animation: wiggle 500ms;
}
There’s just one problem here. When our animation ends, it goes back to the original, pre-animated state. To prevent this, use the animation-fill-mode
property.
The animation-fill-mode
Property
Animations have no effect on properties before they begin or after they stop playing. But as you’ve seen with the wiggle
example, once an animation ends, it reverts to its pre-animation state. With animation-fill-mode
, we can fill in those states before the animation starts and ends.
The animation-fill-mode
property accepts one of four values:
none
: the animation has no effect when it’s not executingforwards
: when the animation ends, the property values of the end state will still applybackwards
: property values for the first keyframe will be applied during the animation delay periodboth
: effects for bothforwards
andbackwards
apply
Since we want our animated element to remain in its final, scaled-up state, we’re going to use animation-fill-mode: forwards
. (animation-fill-mode: both
would also work.)
The effect of animation-fill-mode: backwards
is most apparent when the animation-delay
property is set to 500ms
or higher. When animation-fill-mode
is set to backwards
, the property values of the first keyframe are applied, but the animation isn’t executed until the delay elapses.
Pausing Animations
As has been mentioned, animations can be paused. Transitions can be reversed midway, or stopped altogether by toggling a class name. Animations, on the other hand, can be paused partway through the play cycle using animation-play-state
. It has two defined values—running
and paused
—and its initial value is running
.
Let’s look at a simple example of using animation-play-state
to play or pause an animation. First, our CSS:
.wobble {
animation: wobble 3s ease-in infinite forwards alternate;
animation-play-state: paused;
}
.running {
animation-play-state: running;
}
Here, we have two declaration blocks: wobble
, which defines a wobbling animation, and running
, which sets a play state. As part of our animation
declaration, we’ve set an animation-play-state
value of paused
. To run our animation, we’ll add the running
class to our element. Let’s assume that our markup includes a Run animation button with an id
of trigger
:
const trigger = document.querySelector('#trigger');
const moveIt = document.querySelector('.wobble');
trigger.addEventListener('click', function() {
moveIt.classList.toggle('running');
});
Adding .running
to our element overrides the animation-play-state
value set in .wobble
, and causes the animation to play.
Detecting When Animations Start, End, or Repeat
Like transitions, animations fire an event when they end: animationend
. Unlike transitions, animations also fire animationstart
and animationiteration
events when they begin to repeat. As with transitions, you might use these events to trigger another action on the page. Perhaps you’d use animationstart
to contextually reveal a Stop Animation button, or animationend
to reveal a Replay button.
We can listen for these events with JavaScript. Below, we’re listening for the animationend
event:
const animate = document.getElementById('animate');
animate.addEventListener('animationend', function(eventObject) {
// Do something
});
Here, too, the event handler function receives an event object as its sole argument. In order to determine which animation ended, we can query the animationName
property of the event object.
A Note About Accessibility
Transitions and animations can enhance the user experience by making interactions smooth rather than jumpy, and otherwise bring delight to the interface. But they still have accessibility risks. Large spinning animations, for example, can cause dizziness or nausea for people with vestibular disorders, such as vertigo[5]. Flashing animations can trigger seizures in some people with photosensitive epilepsy[6]. Use them sparingly, and strongly consider giving users the ability to turn them off. We discuss one method for doing this—the prefers-reduced-motion
media query—in Chapter , “Applying CSS Conditionally”.
A Note About Performance
Some properties create better-performing transitions and animations than others. If an animation updates a property that triggers a reflow or repaint, it may perform poorly on low-powered devices such a phones and tablets.
Properties that trigger a reflow are ones that affect layout. These include the following animatable properties:
border-width
(andborder-*-width
properties)border
(andborder-*
properties)bottom
font-size
font-weight
height
left
line-height
margin
(andmargin-*
properties)min-height
min-width
max-height
max-width
padding
(andpadding-*
properties)right
top
vertical-align
width
When these properties are animated, the browser must recalculate the size and position of the affected—and often neighboring—elements. Use transforms where you can. Transitioning or animating translation transforms (for example, transform: translate(100px,200px)
) can replace top
, left
, right
, and bottom
properties. In some cases, height
and width
animations can be replaced with a scale
transformation.
Sometimes, triggering a reflow (or layout update) is unavoidable. In those cases, minimize the number of elements affected and use tricks (such as negative delays) to shorten the perceived animation duration.
Properties that trigger a repaint are typically those that cause a color change. These include:
background
background-image
background-position
background-repeat
background-size
border-radius
border-style
box-shadow
color
outline
outline-color
outline-style
outline-width
Changes to these properties are less expensive to calculate than those that affect layout, but they do still have a cost. Changes to box-shadow
and border-radius
are especially expensive to calculate, especially for low-powered devices. Use caution when animating these properties.