animation-fill-mode: What an Animation Applies Before and After It Runs

Two keyframe bugs account for a large share of animation questions. An element with a delayed fade-in appears at full opacity, then vanishes and fades in — a flash. An element that fades out pops back into view the instant its animation ends. Both have the same cause and the same fix: animation-fill-mode, the property that decides whether an animation's styles apply outside its active period. This page explains the four values precisely, maps each bug to its fix, and shows the less obvious downside of forwards — an animation that keeps overriding styles long after it has finished. It belongs to Keyframe Animation Patterns in the CSS-Only Micro-Interactions & Animations guide.

Why fill mode exists

A CSS animation has an active period — from the end of its delay to the end of its last iteration — and two inactive periods around it: the delay before it starts and the time after it finishes. By default, animation-fill-mode: none, the animation affects nothing outside its active period. The element shows its ordinary styles during the delay, the keyframes during the active period, and its ordinary styles again afterwards.

That default is correct for looping, decorative animations that should leave no trace. It is wrong for most one-shot animations, where the whole point is to move an element from a starting state to an end state and leave it there.

Which styles apply in each period Four rows for none, backwards, forwards and both across three periods: delay, active and after. none shows normal styles in delay and after. backwards shows the first keyframe during the delay. forwards shows the last keyframe after the end. both does both. delay · active · after delay active after none normal keyframes normal backwards first keyframe keyframes normal forwards normal keyframes last keyframe both first keyframe keyframes last keyframe "First" and "last" account for direction: with reverse, they swap.
  • none — keyframes apply only while the animation is active.
  • backwards — during the delay, the element shows the styles of the keyframe the animation will start from. This fixes the delayed-entrance flash.
  • forwards — after the animation ends, the element keeps the styles of the keyframe it finished on. This fixes the exit snap-back.
  • both — backwards and forwards together.

"First" and "last" are determined by animation-direction and animation-iteration-count: an animation running in reverse starts on its to keyframe, and an alternate animation with an even iteration count ends on its from keyframe.

The complete implementation

The demo shows three staggered cards with a delayed entrance under each fill mode. So that it can be replayed without reloading, the demo starts each row's animation when the row is hovered rather than on page load; the fill-mode behaviour is identical. Watch the first row flash.

Live demoDelayed entrances under three fill modes
Hover a row to play its entrance (move away and back to replay): in the none row the delayed cards show, vanish, then fade in; backwards and both hide them until their turn.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>animation-fill-mode</title>
<style>
  body { font: 14px/1.5 system-ui, sans-serif; margin: 1.5rem; display: grid; gap: 1rem; }
  .row { display: flex; gap: 0.5rem; align-items: center; }
  .row code { inline-size: 6rem; }
  .card {
    inline-size: 4rem;
    block-size: 2.5rem;
    border-radius: 6px;
    background: #6366f1;
    /* Each card starts a little later: 0ms, 300ms, 600ms. */
    animation: rise 600ms cubic-bezier(0.22, 1, 0.36, 1) var(--delay, 0ms);
  }
  .card:nth-child(3) { --delay: 300ms; }
  .card:nth-child(4) { --delay: 600ms; }

  @keyframes rise {
    from { opacity: 0; translate: 0 1rem; }
    to   { opacity: 1; translate: 0 0; }
  }

  /* none: delayed cards are visible during their delay, then jump to
     opacity 0 when the animation starts, then fade in. A visible flash. */
  .row--none .card { animation-fill-mode: none; }

  /* backwards: the "from" keyframe applies during the delay. No flash. */
  .row--backwards .card { animation-fill-mode: backwards; }

  /* both: same as backwards here, because the "to" keyframe equals the
     element's normal styles. forwards would add nothing. */
  .row--both .card { animation-fill-mode: both; }

  @media (prefers-reduced-motion: reduce) {
    .card { animation: none; }
  }
</style>
</head>
<body>
  <div class="row row--none"><code>none</code><span class="card"></span><span class="card"></span><span class="card"></span></div>
  <div class="row row--backwards"><code>backwards</code><span class="card"></span><span class="card"></span><span class="card"></span></div>
  <div class="row row--both"><code>both</code><span class="card"></span><span class="card"></span><span class="card"></span></div>
</body>
</html>

The entrance's to keyframe matches the card's normal styles — full opacity, no offset — so forwards adds nothing for it, and backwards is sufficient. That is a useful design habit: write entrances so their final keyframe is the element's resting style, and you never need forwards for them.

The key technique: prefer the resting style over forwards

For exits the situation is reversed: the final keyframe (opacity: 0) is not the element's normal style, so without forwards it snaps back to visible. forwards fixes that, but it comes with a cost worth understanding before reaching for it by default.

forwards keeps winning after the animation ends Left: an element faded out with a forwards animation stays at opacity 0; a later rule setting opacity 1 has no effect because the finished animation still applies. Right: the element's class sets opacity 0 as its own style and the animation only animates toward it; later changes work normally. An animation's fill is still in the cascade fade-out forwards animation ends at opacity 0 later: .is-visible { opacity: 1 } still invisible: animations outrank normal rules end state as own style .is-hidden { opacity: 0; animation: fade-out } later changes work: no lingering fill

Animation styles sit high in the cascade — above all normal author declarations. A finished animation with a forwards fill keeps applying its last keyframe with that priority for as long as the animation remains on the element. Any later attempt to change the same property with an ordinary rule — making the element visible again, changing its position — silently loses. The element seems frozen.

The robust alternative is to make the end state the element's own style and let the animation merely travel toward it: the state class sets opacity: 0 and applies the animation without forwards. When the animation ends, the element's own opacity: 0 takes over seamlessly, and later rules can override it normally. Where an exit genuinely must use forwards — because the element is about to be removed from the DOM anyway — the lingering fill does not matter.

Fill mode with iterations and direction

Loops and alternating animations make "first" and "last" keyframe less obvious, and fill mode follows the animation's actual path rather than the order keyframes are written.

animation-direction: reverse runs from to to from, so backwards applies the to keyframe during the delay and forwards holds the from keyframe at the end. Writing an exit as the reverse of an entrance — one @keyframes rule used both ways — therefore needs no change to the fill mode; the browser picks the correct end.

alternate with an even iteration count ends where it started. Two iterations of a pulse that alternates out and back finish on the from keyframe, so forwards holds the starting look. With an odd count it finishes on to.

Fractional iteration counts end mid-path. animation-iteration-count: 1.5 ends halfway through the second iteration, and forwards holds whatever intermediate value the animation reached. This is occasionally useful for "settle partway" effects but more often a surprise when a count is computed from a custom property.

infinite never ends, so forwards has no effect; only backwards matters, and only if there is a delay.

When an animation's end state looks wrong, working out which keyframe the path finishes on — from its direction and iteration count — usually explains it immediately.

Fill mode and reduced motion

A subtle interaction appears when animations are disabled for reduced motion. If an entrance relies on backwards to hide an element during its delay and the reduced-motion rule sets animation: none, the element simply appears in its normal style — correct. But if an element's resting style is invisible and only a forwards-filled animation makes it visible, removing the animation leaves it invisible forever. That is a real accessibility bug: reduced-motion users never see the content.

The rule that prevents it is the same as above: an element's resting, unanimated style must be its correct final appearance. Animations then only decorate the journey, and removing them can never hide content. prefers-reduced-motion Recipes collects patterns that respect this.

Variation: fill mode with @starting-style instead

For entrances triggered by an element appearing — inserted into the DOM or switched from display: none@starting-style with a transition avoids fill modes altogether. The starting style plays the role of the from keyframe during the first frame, and the element's normal style is the destination:

.toast {
  opacity: 1;
  translate: 0 0;
  transition: opacity 250ms ease-out, translate 250ms cubic-bezier(0.22, 1, 0.36, 1);
}

@starting-style {
  .toast { opacity: 0; translate: 0 1rem; }
}

There is no delay period to fill and no end state to hold, so neither the flash nor the lingering override can occur, and the transition reverses cleanly if the toast is dismissed mid-entrance. The technique is covered fully in @starting-style Entry Animations.

Browser support

animation-fill-mode with none, forwards, backwards and both is supported in every browser with CSS animations; @keyframes dates from Chrome 43, Edge 12, Firefox 16 and Safari 9. @starting-style is supported in Chrome and Edge 117+, Firefox 129+ and Safari 17.5+. The standalone translate property is supported in Chrome and Edge 104+, Firefox 72+ and Safari 14.1+.

FAQ

What does animation-fill-mode: forwards do? It keeps the styles of the animation's final keyframe applied after the animation ends, instead of reverting to the element's normal styles. Without it, an element that animates to opacity: 0 pops back to fully visible when the animation finishes.

What does animation-fill-mode: backwards do? It applies the first keyframe's styles during the animation-delay, before the animation starts. Without it, an element with a delayed fade-in is visible at full opacity during the delay, then jumps to transparent when the animation begins.

Is animation-fill-mode: both always the right choice? It is right for most one-shot entrances and exits, because it covers both the delay and the end. It can hide problems, though: a forwards fill keeps overriding the element's styles indefinitely, so later changes to the same property appear not to work.

Why can't I change a property after a forwards animation? The finished animation's final keyframe is still applied with the high priority animations have in the cascade, overriding ordinary declarations. Remove or replace the animation, or set the final value as the element's normal style and drop the forwards fill.

Related articles

More pages in the same section.