Home / Articles / Animation & Motion /
animation-fill-mode: forwards, backwards, both
CSS animations have a before, a during, and an after. Fill mode controls what happens in the gaps — and getting it wrong causes elements to snap back unexpectedly.
The problem fill mode solves
By default, a CSS animation only affects an element while it's running. Before the delay expires and after the animation ends, the element reverts to its original styles. This causes a visible snap.
@keyframes fade-in {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
/* Without fill mode — element starts visible,
jumps to opacity: 0 when animation begins,
then snaps back to original opacity after it ends */
.element {
animation: fade-in 0.4s ease;
}
The element flickers because its resting state has no opacity rule, so it reverts to the default opacity: 1 after the animation completes.
forwards — hold the final keyframe
animation-fill-mode: forwards keeps the element styled with the values from the last keyframe after the animation finishes.
.element {
opacity: 0;
animation: fade-in 0.4s ease forwards;
}
/* After animation ends, element stays at:
opacity: 1; transform: translateY(0); */
This is the most commonly used fill mode. It's essential for one-shot entrance animations where the element should remain in its final state.
backwards — apply the first keyframe during delay
animation-fill-mode: backwards applies the from keyframe values during the animation-delay period. Without it, the element sits in its original state until the delay expires.
.element {
animation: fade-in 0.4s ease 0.5s backwards;
}
/* During the 0.5s delay, element shows:
opacity: 0; transform: translateY(12px);
— not its default styles */
This matters most for staggered animations. Without backwards, elements briefly appear in their final position before jumping to the start of the animation when the delay expires.
both — the safest default
animation-fill-mode: both combines forwards and backwards. The first keyframe applies during the delay, and the last keyframe persists after completion.
.stagger-item {
animation: fade-in 0.3s ease both;
}
.stagger-item:nth-child(1) { animation-delay: 0s; }
.stagger-item:nth-child(2) { animation-delay: 0.1s; }
.stagger-item:nth-child(3) { animation-delay: 0.2s; }
.stagger-item:nth-child(4) { animation-delay: 0.3s; }
both. It covers every edge case and has no performance cost over the other values.none — the default behavior
animation-fill-mode: none is the initial value. The animation affects the element only while running. Before and after, the element's own style rules apply.
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
/* Looping animation — fill mode doesn't matter */
.icon {
animation: pulse 2s ease infinite;
/* animation-fill-mode: none (default) is fine */
}
For infinite animations, fill mode is irrelevant because the animation never ends. The none default is perfectly appropriate.
Fill mode with animation-direction
When animation-direction is reverse or alternate, the meaning of "first" and "last" keyframe changes. Fill mode follows the effective direction.
@keyframes slide-out {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.exiting {
animation: slide-out 0.3s ease forwards;
/* Stays at: translateX(100%), opacity: 0 */
}
.entering {
animation: slide-out 0.3s ease reverse both;
/* backwards: starts at the "to" keyframe
forwards: ends at the "from" keyframe */
}
Using reverse with fill mode lets you reuse a single @keyframes block for both entrance and exit, keeping your animation library DRY.