Home / Articles / Animation & Motion /

animationkeyframes

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; }
When in doubt, use 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.