animation-fill-mode Explained
Part of CSS Transitions vs Animations in Core CSS Animation Fundamentals.
The problem
A list of cards fades in with staggered delays. On load, every card appears fully visible for a moment, disappears, then fades in — a flash. Adding forwards fixes nothing. Adding both fixes the flash, but now the cards cannot be hovered: their transform is stuck at the animation’s last keyframe and the hover rule has no effect. Removing the animation class to “reset” them makes them jump.
animation-fill-mode controls what the element looks like outside the animation’s active time. Most confusion comes from not knowing that “outside” includes both the delay before and the time after.
Root cause analysis: the active interval and its edges
An animation has an active interval: from the end of its delay to the end of its last iteration. Inside that interval, keyframe values apply. Outside it, animation-fill-mode decides.
none— the default. Before the delay ends and after the animation finishes, the element uses its normal styles. A delayed fade-in shows the element at full opacity during the delay, then snaps toopacity: 0when the animation starts: the flash.backwards— during the delay, apply the values of the keyframe the animation will start on. For a normal direction that is the0%keyframe; forreverseoralternate-reverseit is100%. The delayed card stays invisible until its turn.forwards— after the animation ends, keep the values of the keyframe it ended on. For an even number ofalternateiterations that is the0%keyframe, not100%.both— backwards before, forwards after.
Filled values are animation values. They sit in the animation origin of the cascade, above normal rules including :hover. A card whose entrance fills forwards on transform has its transform controlled by the finished animation indefinitely, which is why the hover rule appears to do nothing — the mechanism behind the “dead hover” described in debugging the animation effect stack.
Step-by-step resolution
Production code pattern
/* Staggered entrance: backwards fill, end state is the base style. */
.card {
/* Base style IS the end state: visible, in place. */
opacity: 1;
translate: 0 0;
animation: card-in 300ms cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
animation-delay: calc(var(--i, 0) * 60ms);
transition: translate 160ms ease-out;
}
@keyframes card-in {
from { opacity: 0; translate: 0 12px; } /* only a from: the to is the base style */
}
.card:hover { translate: 0 -4px; } /* works: no forwards fill holding translate */
/* A genuine forwards case: a one-time "highlight" that should stay tinted
until the user dismisses it, without adding a class in script. */
.notice.is-new {
animation: notice-flash 800ms ease-out forwards;
}
@keyframes notice-flash {
from { background-color: var(--color-accent-100); }
to { background-color: var(--color-accent-50); }
}
.notice.is-new.is-read { animation: none; } /* dismissal removes the fill */
@media (prefers-reduced-motion: reduce) {
.card { animation: none; } /* base style is already the end state */
.notice.is-new { animation: none; background-color: var(--color-accent-50); }
}
Rendering Impact: composite for the card entrance. Fill mode does not change per-frame cost, but a forwards fill keeps the property under animation control after motion ends, which can keep a layer promoted and blocks transitions on the same property.
Writing entrance keyframes with only a from block is the habit that makes forwards unnecessary. The browser builds the missing to keyframe from the element’s current base style, so the animation ends exactly where normal CSS already puts the element, and nothing needs to be held.
Verification checklist
Constraints and trade-offs
backwardsapplies during the delay only; it does nothing for animations without a delay.- Removing an animation that fills forwards causes an instant jump to base styles.
- Fractional iteration counts fill with interpolated mid-animation values.
bothis a common default in snippets and is often more than needed.- Script animations use the
filloption with the same values and the same pitfalls.
Frequently asked questions
Why does my delayed animation flash before it starts?
With the default fill mode none, the element shows its base styles during the delay. Use backwards so the first keyframe applies until the animation starts.
Why can’t I change a property after an animation with forwards finishes?
The filled value belongs to the animation, which overrides normal rules. Put the end state in base CSS and remove forwards, or remove the animation when the state changes.
What does animation-fill-mode: both do?
It applies the starting keyframe during the delay and keeps the ending keyframe after the animation finishes.
Does forwards always hold the 100% keyframe?
No. It holds whichever keyframe the final iteration ended on, which depends on direction and iteration count.
Related
- CSS Transitions vs Animations — the parent topic
- Debugging the Animation Effect Stack — tracing fills that override rules
- commitStyles() & Persisting End State — the script alternative to fills