Probably the most confusing aspect of keyframe animations is fill modes. They're the biggest obstacle on our path towards keyframe confidence.
Let's start with a problem.
We want our element to fade out. The animation itself works fine, but when it's over, the element pops back into existence:
<style>
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.box {
animation: fade-out 1000ms;
}
</style>
If we were to graph the element's opacity over time, it would look something like this:
Why does the element jump back to full visibility? Well, the declarations in the from
and to
blocks only apply while the animation is running.
After 1000ms has elapsed, the animation packs itself up and hits the road. The declarations in the to
block dissipate, leaving our element with whatever CSS declarations have been defined elsewhere. Since we haven't set opacity
for this element anywhere else, it snaps back to its default value (1
).
One way to solve this is to add an opacity
declaration to the .box
selector:
<style>
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.box {
animation: fade-out 1000ms;
/*
Change the "default" value for opacity,
so that it reverts to 0 when the
animation completes.
*/
opacity: 0;
}
</style>
<div class="box">
Hello World
</div>
While the animation is running, the declarations in the @keyframes
statement overrule the opacity declaration in the .box
selector. Once the animation wraps up, though, that declaration kicks in and keeps the box hidden.
Specificity?
In CSS, conflicts are resolved based on the βspecificityβ of a selector. An ID selector (#login-form
) will win the battle against a class one (.thing
).
But what about keyframe animations? What is their specificity?
It turns out that specificity isn't really the right way to think about this; instead, we need to think about cascade origins.A βcascade originβ is a source of selectors. For example, browsers come with a bunch of built-in stylesβthat's why anchor tags are blue and underlined by default. These styles are part of the User-Agent Origin.
The specificity rules only apply when comparing selectors in the same origin. The styles we write normally are part of the βAuthor Originβ, and Author Origin styles win out over ones written in the User-Agent Origin.
Filling forwards
Instead of relying on fallback declarations, let's consider another approach, using animation-fill-mode
:
<style>
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.box {
animation: fade-out 1000ms;
animation-fill-mode: forwards;
}
</style>
<div class="box">
Hello World
</div>
animation-fill-mode
lets us persist the final value from the animation, forwards in time.
"forwards" is a very confusing name, but hopefully seeing it on this graph makes it a bit clearer!
When the animation ends, animation-fill-mode: forwards
will copy/paste the declarations in the final block, persisting them forwards in time.
Filling backwards
We don't always want our animations to start immediately! As with transition, we can specify a delay, with the animation-delay property.
Unfortunately, we run into a similar issue:
<style>
@keyframes slide-in {
from {
transform: translateX(-100%);
opacity: 0.25;
}
to {
transform: translateX(0%);
opacity: 1;
}
}
.box {
animation: slide-in 1000ms;
animation-delay: 500ms;
}
</style>
<div class="box">
Hello World
</div>
For that first half-second, the element is fully visible!
The CSS in the from
and to
blocks is only applied while the animation is running. Frustratingly, the animation-delay
period doesn't count. So for that first half-second, it's as if the CSS in the from
block doesn't exist.
animation-fill-mode
has another value that can help us here: backwards
. This will apply the CSS from the first block backwards in time.
βForwardsβ and βbackwardsβ are confusing values, but here's an analogy that might help: imagine if we had recorded the user's session from the moment the page loaded. We could scrub forwards and backwards in the video. We can scrub backwards, before the animation has started, or forwards, after the animation has ended.
<style>
@keyframes slide-in {
from {
transform: translateX(-100%);
opacity: 0.25;
}
to {
transform: translateX(0%);
opacity: 1;
}
}
.box {
animation: slide-in 1000ms;
animation-delay: 500ms;
animation-fill-mode: backwards;
}
</style>
<div class="box">
Hello World
</div>
What if we want to persist the animation forwards and backwards? We can use a third value, both, which persists in both directions:
Personally, I wish that both
was the default value. It's so much more intuitive! Though it can make it a bit harder to understand where a particular CSS value has been set.
Like all of the animation properties we're discussing, it can be tossed into the animation
shorthand salad:
.box {
animation: slide-in 1000ms ease-out both;
animation-delay: 500ms;
}
Oldest comments (2)
Very cool explanation with schemas !
I rated your post to hight quality, keep going!
Thank you, Thomas !