Taking the scenic route
When you create transitions, you set a start value and an end value, and then animate from one to the other. They’re perfect for animating things like the :hover
state of a button, where you want to go from color A to color B. No muss, no fuss. You can use acceleration curves so the journey isn’t just a linear shot between the two, but you’re still going from point A to point B.
But what if I want to add a few more points in there?
Instead of going from point A to point B, maybe you want to go to point C first.
Let’s take a look at the progress bar that we created back in Part 2, when you learned about transform origin:
It’s okay, but anyone who has ever downloaded anything from the internet knows that connection speeds are anything but constant. Progress comes in bursts, interspersed with pauses and lulls. If you look at the acceleration curve of our transition, it starts out quickly before tapering off towards the end, but the progress still follows a smooth path.
To create a more authentic-feeling progress bar, you can change it from a transition to a keyframed animation and make use of CSS @keyframes to create some hiccups and bursts to the rhythm of the loading bar. While CSS transitions only go from one value to another, @keyframes let you build animations with multiple steps or stages, allowing you to create more complex and dynamic animations.
Imagine that we’ve built an obstacle course. To get from the starting line to the finishing, competitors need to run through some tires, climb over a wall, and swing across a set of monkey bars. And between each of the obstacles is a checkpoint, ensuring that they complete the full course, rather than cheating by running directly to the finish line. Think of @keyframes as those checkpoints, controlling the route our animations take and what happens along the way.
Stop and go: keyframes
Coming back to our loading bar, we can insert a few keyframes into the middle of the animation to flatten and steepen the slope of the acceleration of the curve, creating those bursts that we’re looking for. A CSS keyframe is defined by the percentage of animation completed when its value is realized.
To translate our progress bar transition to a keyframes animation, the start would have a progress of 0%, and the transform property a value of scaleX(0). The end would have a progress of 100%, and a value of scaleX(1). Now that we know what we want our first two keyframes to be, let’s build our first set of keyframes!
Unlike transitions, which are one-time-use only and exist solely within the selector where they’ve been declared, keyframes are available globally. Any selector within a CSS file can use them. And since they’re available globally, they aren’t declared within a selector. Instead, declare keyframes at the base level of the CSS file, using the @keyframes
operator, followed by a name of your choosing and a set of curly braces:
@keyframes progress-bar{
}
When you use @keyframes
, you’re declaring a set of keyframes and giving it a name which you can use to call the animation on any selector. Now that we’ve created an @keyframes
set for our progress bar, let’s add starting, and ending keyframes.
Within the set’s curly braces, define each keyframe using its percentage, then its own set of curly braces, and the CSS properties and values you would like applied at that stage of the animation:
@keyframes progress-bar{
0% {
transform: scaleX(0);
}
100% {
transform: scaleX(1);
}
}
Now we have a set of keyframes named “progress-bar” that animates from a scaleX() of zero up to 1 over the course of its animation like the transition we built back in Part 2.
In the case that we only wanted starting and ending keyframes with nothing in the middle rather than percentages, we can define the keyframes using the from and to keywords:
@keyframes progress-bar{
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
The from keyword acts exactly the same as 0%, and to acts just like 100%, so it comes down to preferences/established code styles when choosing which to implement.
So… what now?
Setting things in motion
Let’s assign it to the :active
pseudo-selector of our button, like we did with our transition via the animation-name
and animation-duration
properties. animation-duration
looks a lot like transition-duration
, and behaves exactly the same, accepting a time value with units of either ms (milliseconds) or s (seconds).
On the other hand, animation-name
is new. This is where you assign the keyframes that you’ve just made to your triggering element via the name of the @keyframes
set. So, to use progress-bar keyframes, we assign the animation-name
property with a value of progress-bar:
.btn {
&:active {
& > .progress__bar {
transform: scaleX(1);
animation-name: progress-bar;
}
}
}
@keyframes progress-bar{
0% {
transform: scaleX(0);
}
100% {
transform: scaleX(1);
}
}
And let’s make the duration the same as our transition duration by assigning the animation-duration
property a value of 1000ms:
.btn {
&:active {
& > .progress__bar {
transform: scaleX(1);
animation-name: progress-bar;
animation-duration: 1000ms;
}
}
}
@keyframes progress-bar{
0% {
transform: scaleX(0);
}
100% {
transform: scaleX(1);
}
}
And that’s it! Transitions animate between the value of a CSS property and the value assigned to its triggering element, meaning that you need to have values assigned to selectors within your code. @keyframes animations are different. When animation-name
and animation-duration
are assigned to a selector, the properties and values contained within each keyframe are applied throughout the course of the animation.
This means that you don’t need to worry about setting the value of scaleX()
like with our progress-bar transition. We don’t, however, need to remove it. In the case of conflicting CSS properties/values between an assigned value and a keyframed value, CSS will override the assigned value with the keyframed value.
While unnecessary, let’s remove the transform property from the : hover
state for the sake of clarity:
.btn {
&:active {
& > .progress__bar {
animation-name: progress-bar;
animation-duration: 1000ms;
}
}
}
@keyframes progress-bar{
0% {
transform: scaleX(0);
}
100% {
transform: scaleX(1);
}
}
And with that, we’re all set!
Or are we…? Like transitions, you can consolidate all of your animation properties into a single one that includes its name, duration, etc. Let’s clean things up a little more and consolidate all of our animation properties into the animation
property:
.btn {
&:active {
& > .progress__bar {
animation: progress-bar 1000ms;
}
}
}
@keyframes progress-bar{
0% {
transform: scaleX(0);
}
100% {
transform: scaleX(1);
}
}
Now that we’ve gotten that tidied up, let’s hop onto the browser and see how our new keyframed progress bar performs:
Where’d it go?! It animates on as we’d expected, but then it disappears? We’ll go into detail about the differences between transitions and animations made with @keyframes in the next chapter. For now, just know that the two behave differently and to use the animation properties to apply @keyframes to your selectors.
Break it up! — multiple keyframes
We have an animated progress bar that zips straight from 0% to 100%, just like our transition. But we wanted to break up the animation by sticking a few keyframes in the middle. So what percentages should the keyframes have? And what values should we assign them? Let’s use the acceleration curve from our original progress bar transition as a template:
The X-axis shows the percentage of the animation completed, from 0% at the origin to 100% at the far right. And the Y-axis shows the value for scaleX ()
from 0 at the bottom up to 1 at the top.
If you were to pick a point along the acceleration curve, its X-coordinate could serve as a keyframes percentage value, and its Y-coordinate as the value for scaleX()
:
Let’s pick three spots along the curve to turn into keyframes. Since we want to break up the animation and make it feel more natural, let’s try to choose them at random intervals on the X-axis. Something like this could work:
Now that we have some values to work with, let’s make some new keyframes! To build new keyframes, we’ll take the X-coordinate of each point, turn it into the percentage value for each of our new keyframes, and use the Y-coordinate as the value for the value of scaleX()
, like so:
.btn {
&:active {
& > .progress__bar {
animation: progress-bar 1000ms;
}
}
}
@keyframes progress-bar{
0% {
transform: scaleX(0);
}
17% {
transform: scaleX(.18);
}
24% {
transform: scaleX(.4);
}
46% {
transform: scaleX(.81);
}
100% {
transform: scaleX(1);
}
}
Let’s take our new keyframes for a spin and see how the animation plays out:
The bar fills in with a similar profile to our transition, but each time the animation hits a keyframe, there is a brief pause in its progress, which breaks the animation up and feels more like it's loading something.
The more the merrier: multiple properties
So, we have a progress bar that behaves much more naturally, but let’s say that beyond scaling the bar up, we’d also like it to animate its opacity, so it evolves from a translucent green to its final minty-green magnificence as the progress bar fills in. When you create CSS @keyframes, you’re not limited to assigning singular properties to them. In fact, you can assign as many properties to each keyframe as you’d like!
To add additional properties to a keyframe, add them within the curly brackets, just as you would with a standard CSS selector. Let’s set up our keyframes so the bar will have an opacity of 10% at the start of the animation:
@keyframes progress-bar{
0% {
transform: scaleX(0);
opacity: .1;
}
17% {
transform: scaleX(.18);
}
24% {
transform: scaleX(.4);
}
46% {
transform: scaleX(.81);
}
100% {
transform: scaleX(1);
}
}
We’ve set the opacity to 10% in the first keyframe, but don’t have any other keyframes controlling the opacity for the rest of the animation. Now, it might seem like the bar would remain 10% opaque throughout the rest of the animation, but when we check out how it behaves when we trigger it in the browser, we find that isn’t the case at all:
Despite not having any other keyframes for opacity set, the bar still animates from 10% opacity to 100% over the course of the animation. What gives?
Since we haven’t given any values for the opacity property to animate towards, the browser will use whatever the element’s assigned opacity value is. And since we don’t have the opacity value assigned to .progress__bar
, it assumes the default value is 1.
For the sake of clarity, let’s assign .progress__bar
the opacity property with a value of 0 and up our initial opacity to 50%:
.progress {
&__bar {
opacity: 0;
}
}
@keyframes progress-bar{
0% {
transform: scaleX(0);
opacity: .5;
}
17% {
transform: scaleX(.18);
}
24% {
transform: scaleX(.4);
}
46% {
transform: scaleX(.81);
}
100% {
transform: scaleX(1);
}
}
Now our bar should start at 10% at the beginning and animate to completely transparent at the end:
And, if we don’t supply a starting keyframe, the browser will start the animation with the selector’s assigned value, just like it does at the end of an animation if we don’t supply an ending keyframe. So if we were only to set the opacity value at an intermediate percentage, like so:
.progress {
&__bar {
opacity: 0;
}
}
@keyframes progress-bar{
0% {
transform: scaleX(0);
}
17% {
transform: scaleX(.18);
}
24% {
transform: scaleX(.4);
}
46% {
transform: scaleX(.81);
opacity: 1;
}
100% {
transform: scaleX(1);
}
}
Our bar will now start and end with the opacity value that we’ve assigned in .progress__bar, which is 0:
A fully transparent progress bar doesn’t make for the best user experience, so let’s set a keyframe with an opacity value of 1, but rather than waiting until the animation has fully completed to become fully opaque, let’s set the keyframe’s percentage to 85%:
.progress {
&__bar {
opacity: 0;
}
}
@keyframes progress-bar{
0% {
transform: scaleX(0);
}
17% {
transform: scaleX(.18);
}
24% {
transform: scaleX(.4);
}
46% {
transform: scaleX(.81);
}
85% {
opacity: 1;
}
100% {
transform: scaleX(1);
}
}
Now our bar should spend 85% of the animation’s duration filling in the opacity. Let’s check it out:
Well…. it does spend 85% of the animation increasing the opacity of the bar, but then it spends the final 15% animating back to a completely transparent bar. Not quite what we were hoping for!
We still don’t have a value for the opacity property to animate towards between 85% and 100%, so the browser spends the final 15% animating towards the value assigned to .progress__bar, which is still 0. So, we need a final keyframe for the opacity property, with a value set to 1.
We’ve already created a keyframe with a percentage of 100% for our transform property. We could add the opacity property to that keyframe, but what if we decided that we wanted our final opacity to be 85%, not 100%? Then we need to change the opacity value in multiple locations. This means more work (boo!!!), not to mention an extra opportunity to make a mistake and not update both with the same values.
So far, we’ve set a single percentage value to each keyframe, but we can assign multiple ones. By adding multiple percentages to a keyframe, the browser will apply its contents at each animation percentage as long as we separate them with commas:
.progress {
&__bar {
opacity: 0;
}
}
@keyframes progress-bar{
0% {
transform: scaleX(0);
}
17% {
transform: scaleX(.18);
}
24% {
transform: scaleX(.4);
}
46% {
transform: scaleX(.81);
}
85%,100% {
opacity: 1;
}
100% {
transform: scaleX(1);
}
}
And now, if we take our progress bar for a spin again, things should look just like we’d expect:
Perfect! Now our opacity reaches 1 at 85% and stays there through the rest of the animation.
Coming up, go into greater detail about implementing keyframed animations in CSS, and how you can push your pages further through other animation properties.
Let's recap!
CSS @keyframes let you build more complex animations by allowing you to create multiple phases or waypoints for properties throughout the course of the animation.
CSS keyframes are instantiated using the @keyframes at-rule followed by a name for the set:
@keframes example-frames {...}
Each keyframe can be established using the percentage of animation completed as its value:
33% {...}
In the case of only needing a starting and ending keyframe, the keywords from and to can be used in place of 0% and 100%, respectively.
If no starting or ending keyframes are supplied, the animation will start and/or end with the property values assigned to the selector.
A set of keyframes can contain separate keyframes for separate properties.
Multiple percentages can be assigned to a single keyframe, where the contained property values will be applied at those percentage points in the animation.
The properties and values in a set of keyframes will override property values assigned to a selector during the course of the animation.