CSSanimation-fill-mode

animation-fill-mode

animation-fill-mode controls what styles an element has before the animation starts and after it ends. Options are none (default, reverts to original), forwards (stays at final state), backwards (applies starting state immediately), both (applies both). Essential for animations that shouldn't snap back, delayed animations that should show initial state early, and entrance/exit animations.

Fill mode values

Value

Before Start

After End

Use Case

none (default)

Original styles

Original styles

Temporary animations

forwards

Original styles

Final frame

Animations that persist (buttons, toggles)

backwards

First frame

Original styles

Delayed animations with easing in

both

First frame

Final frame

Entrance/exit animations, delayed

CSS
/* none: default, no fill */
.none-fill {
  opacity: 0;
  /* Starts as opacity: 0 in CSS */
  animation: fade-in 1s ease;
  animation-fill-mode: none;
  /* Before animation: opacity 0 */
  /* During animation: fades to 1 */
  /* After animation: back to opacity 0 */
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* forwards: stays at final state */
.forwards-fill {
  animation: fade-in 1s ease;
  animation-fill-mode: forwards;
  /* Before animation: original styles */
  /* During animation: fades from 0 to 1 */
  /* After animation: stays at opacity 1 */
}

/* backwards: applies starting state immediately */
.backwards-fill {
  animation: fade-in 1s ease;
  animation-delay: 0.5s;
  animation-fill-mode: backwards;
  /* Before animation (before delay): opacity 0 (first frame) */
  /* During delay: shows opacity 0 */
  /* During animation: fades to 1 */
  /* After animation: back to original opacity */
}

/* both: combines forwards and backwards */
.both-fill {
  animation: fade-in 1s ease;
  animation-delay: 0.5s;
  animation-fill-mode: both;
  /* Before animation (before delay): opacity 0 (first frame) */
  /* During delay: shows opacity 0 */
  /* During animation: fades to 1 */
  /* After animation: stays at opacity 1 (final frame) */
}
Practical examples

CSS
/* Button entrance: use forwards to stay transformed */
.button-entrance {
  animation: button-pop 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
  animation-fill-mode: forwards;
  /* Bounces in and stays at normal size */
}

@keyframes button-pop {
  from { transform: scale(0); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

/* Modal fade-in: use forwards to stay visible */
.modal {
  animation: fade-in 0.3s ease;
  animation-fill-mode: forwards;
  /* Fades in and stays visible */
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Delayed entrance with backwards: shows initial state during delay */
.delayed-entrance {
  animation: slide-up 0.6s ease;
  animation-delay: 0.3s;
  animation-fill-mode: both;
  /* Shows starting position (y: 30px) during delay */
  /* Slides up during animation */
  /* Stays at final position after */
}

@keyframes slide-up {
  from { opacity: 0; transform: translateY(30px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Loading spinner: use none (animation loops) */
.spinner {
  animation: spin 1s linear infinite;
  animation-fill-mode: none;
  /* No fill needed for infinite animations */
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

/* Toggle animation: use forwards to persist */
.toggle {
  background: white;
  animation: toggle-change 0.3s ease;
  animation-fill-mode: forwards;
  /* Animates to new color and stays */
}

.toggle.active {
  animation: toggle-active 0.3s ease;
  animation-fill-mode: forwards;
  /* Different animation for toggling */
}

@keyframes toggle-change {
  from { background: white; }
  to { background: #f0f0f0; }
}

@keyframes toggle-active {
  from { background: #f0f0f0; }
  to { background: #3498db; }
}
Animation fill mode with delays

CSS
/* The difference between fill modes with delay */

/* SCENARIO: animation with animation-delay: 0.5s */

/* none: element invisible during delay */
.none {
  animation: fade-in 1s ease;
  animation-delay: 0.5s;
  animation-fill-mode: none;
  /* Time 0-0.5s: shows original style (invisible) */
  /* Time 0.5-1.5s: animates */
  /* Time 1.5s+: back to original (invisible) */
}

/* forwards: element invisible during delay, stays visible after */
.forwards {
  animation: fade-in 1s ease;
  animation-delay: 0.5s;
  animation-fill-mode: forwards;
  /* Time 0-0.5s: shows original style (invisible) */
  /* Time 0.5-1.5s: animates */
  /* Time 1.5s+: stays at final state (visible) */
}

/* backwards: element visible during delay, back to original after */
.backwards {
  animation: fade-in 1s ease;
  animation-delay: 0.5s;
  animation-fill-mode: backwards;
  /* Time 0-0.5s: shows first frame (visible) */
  /* Time 0.5-1.5s: animates */
  /* Time 1.5s+: back to original style (invisible) */
}

/* both: element visible during delay, stays visible after */
.both {
  animation: fade-in 1s ease;
  animation-delay: 0.5s;
  animation-fill-mode: both;
  /* Time 0-0.5s: shows first frame (visible) */
  /* Time 0.5-1.5s: animates */
  /* Time 1.5s+: stays at final state (visible) */
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Staggered list with delayed entrance and forwards */
.list-item {
  animation: list-entrance 0.5s ease;
  animation-fill-mode: both;
  /* Each item shows start position during delay, */
  /* animates in, and stays in final position */
}

.list-item:nth-child(1) { animation-delay: 0s; }
.list-item:nth-child(2) { animation-delay: 0.1s; }
.list-item:nth-child(3) { animation-delay: 0.2s; }
.list-item:nth-child(4) { animation-delay: 0.3s; }

@keyframes list-entrance {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Page load animations: use both for best effect */
.page-element {
  animation: entrance 0.6s cubic-bezier(0.4, 0, 0.2, 1);
  animation-delay: calc(var(--index) * 0.1s);
  animation-fill-mode: both;
  /* Each element appears to load in sequence */
}
Common gotchas and solutions

CSS
/* ❌ GOTCHA: Animation snaps back when it shouldn't */
.snaps-back {
  animation: fade-in 1s ease;
  animation-fill-mode: none;
  /* After animation, element becomes invisible again */
}

/* ✓ SOLUTION: Use forwards */
.stays-visible {
  animation: fade-in 1s ease;
  animation-fill-mode: forwards;
  /* Stays in final state */
}

/* ❌ GOTCHA: Delayed animation looks glitchy */
.glitchy {
  opacity: 0;
  animation: fade-in 1s ease;
  animation-delay: 0.5s;
  animation-fill-mode: none;
  /* Element is invisible during delay (jarring) */
}

/* ✓ SOLUTION: Use both for smooth entrance */
.smooth-delayed {
  animation: fade-in 1s ease;
  animation-delay: 0.5s;
  animation-fill-mode: both;
  /* Element shows starting state during delay (smooth) */
}

/* ❌ GOTCHA: Multiple animations fighting */
.fighting {
  animation: scale-up 1s ease forwards;
  animation: fade-in 1s ease forwards; /* Overwrites scale-up */
}

/* ✓ SOLUTION: Use animation shorthand or separate them */
.correct {
  animation:
    scale-up 1s ease forwards,
    fade-in 1s ease forwards;
  /* Both animations play */
}

/* ❌ GOTCHA: Infinite animation with forwards/backwards (no effect) */
.no-effect {
  animation: spin 2s linear infinite;
  animation-fill-mode: forwards;
  /* forwards/backwards/both don't matter for infinite */
}

/* ✓ FINE: Infinite animations don't need fill-mode */
.infinite {
  animation: spin 2s linear infinite;
  /* No fill-mode needed */
}

/* Common pattern: entrance + persistence */
@keyframes entrance {
  from { opacity: 0; transform: scale(0.9); }
  to { opacity: 1; transform: scale(1); }
}

.element {
  animation: entrance 0.5s ease;
  animation-fill-mode: forwards;
  /* Animates in and stays */
}
Combining with animation-play-state

CSS
/* Fill mode works with paused animations */

.pausable {
  animation: pulse 2s ease-in-out infinite;
  animation-fill-mode: forwards;
  animation-play-state: running;
}

.pausable:hover {
  animation-play-state: paused;
  /* Pauses at current frame, stays there */
  /* fill-mode determines what happens when restarted */
}

/* Animation fill with JavaScript control */
.js-controlled {
  animation: fade-in 0.5s ease;
  animation-fill-mode: forwards;
  animation-play-state: paused;
  /* Starts paused, visible at start frame with backwards */
  /* Can be played on-demand */
}

/* Progressive enhancement */
.progressive {
  /* CSS fallback */
  transition: opacity 0.3s ease;

  /* Enhanced with animation */
  animation: fade-in 0.5s ease;
  animation-fill-mode: forwards;
  /* Better visual effect when animations supported */
}
Note
`animation-fill-mode` controls styles before/after animation: `none` = original (default), `forwards` = stays at final state (use for persistent changes), `backwards` = applies first frame immediately (use for eased-in delays), `both` = applies first frame and stays at final state (best for delayed entrances). Never needed for infinite animations. Use forwards for entrances/exits that should persist. Use both with animation-delay for smooth staggered reveals.
Next
Animation play state: [animation-play-state](/css/animation-play-state).