In CSS, what does animation-fill-mode do, and what are the practical differences between none, forwards, backwards, and both?
answer
- controls the interval outside the run
- default is none: nothing before or after
- the delay window versus after the end
- forwards means last keyframe encountered
- direction decides which keyframe that is
basics
~20 sanimation-fill-mode decides whether keyframe values apply outside the animation's active run. The default none applies nothing before or after; forwards holds the final keyframe after it ends; backwards applies the first keyframe during the delay; both does each.
solid answer
~50 sAn animation only controls its element between its start and its end. `animation-fill-mode` extends that control outward. With the default `none`, the element renders with its cascaded values during `animation-delay` and again the instant the animation finishes — which is why a fade-out with no fill mode snaps back to full opacity at the end. `forwards` keeps the values from the last keyframe of the final iteration applied indefinitely. `backwards` applies the first keyframe's values immediately, during the delay, so a delayed fade-in does not sit fully visible while it waits. `both` does the two together, which is the usual choice for delayed entrance animations. The key subtlety is that a filled value does not change the element's cascaded value — it is still an animated value sitting above normal declarations, so a plain rule cannot override it, but an `!important` one can.
code
css · 14 lines@keyframes fade-in-up {
from { opacity: 0; translate: 0 12px; }
to { opacity: 1; translate: 0 0; }
}
.toast {
opacity: 1;
animation: fade-in-up 300ms ease-out 400ms both;
}
.pinned-open {
/* even iteration count + alternate ends on the 0% keyframe */
animation: fade-in-up 300ms ease-out 2 alternate forwards;
}go deeper
Know that by default an animation leaves no trace once it ends, and that animation-fill-mode: forwards is what makes the final state stick. Being able to name the four values is enough here.
Explain the active interval and what each value does on either side of it, and work out which keyframe forwards holds once animation-direction: alternate and the iteration count are involved.
Demonstrate the cascade consequence — a filled value outranks normal declarations, so later classes appear ignored — and argue for ending the animation in the resting state or applying an end-state class on animationend instead.
Own the convention across a codebase: whether persisted end states are expressed as fills or as cascade state, since fills scattered through a stylesheet make element state unreadable from the rules alone.
## The active interval A keyframe animation has an *active interval*: it starts after `animation-delay` and ends after `animation-duration × animation-iteration-count`. Inside that interval, the animation drives every property that appears in its `@keyframes` rule. Outside it — before the start and after the end — the animation contributes nothing at all by default, and the element renders with the values the cascade gives it. `animation-fill-mode` is the switch for what happens outside that interval. ## The four values **`none`** (the initial value): the animation contributes nothing before or after. This produces the two classic surprises. A fade-out animation reaches `opacity: 0` at the last keyframe and then, one frame later, snaps back to the element's declared opacity — usually `1`. And a delayed fade-in sits fully visible during its delay, then jumps to `opacity: 0` when the animation actually starts. **`forwards`**: after the active interval ends, the element keeps the values from the last keyframe *encountered* on the final iteration. That wording matters — see the direction interaction below. **`backwards`**: during the delay period, the element already renders with the values of the first keyframe it *will* use. That is normally the `0%` block, but with `animation-direction: reverse` or `alternate-reverse` the first keyframe it will use is `100%`. **`both`**: `backwards` behaviour before, `forwards` behaviour after. ```css @keyframes fade-in-up { from { opacity: 0; translate: 0 12px; } to { opacity: 1; translate: 0 0; } } .toast { animation: fade-in-up 300ms ease-out 400ms both; } ``` Without `both`, this toast would be fully opaque and in place for 400ms, then jump down and vanish before animating in. ## The direction and iteration-count interaction `forwards` does *not* mean "the 100% keyframe". It means the end of the last iteration, which depends on `animation-direction` and `animation-iteration-count`: - `normal`, any whole count → ends at `100%`. - `reverse` → every iteration runs backwards, so it ends at `0%`. - `alternate` with an **even** count → the final iteration ran backwards, so it ends at `0%`. - `alternate` with an **odd** count → ends at `100%`. A fractional `animation-iteration-count` fills with the value at that fractional point: `animation: slide 2s 0.5 forwards` holds the interpolated midpoint of the keyframes forever. ## Filled values and the cascade A filled value is still an *animated* value. In the cascade, animations sit above normal author declarations, so while a `forwards` fill is in effect, a plain rule targeting the same property does not visibly win no matter how specific it is: ```css .panel { opacity: 1; } /* loses to the fill */ .panel.hidden { opacity: 1 !important; } /* wins — important author beats animations */ ``` The element's *cascaded* value never changed. `element.style` is still empty, so script that reads the inline style sees nothing, while `getComputedStyle` reports the filled value. This trips people up when they try to "take over" from an animation by setting a class. ## When forwards is the wrong tool `forwards` keeps the animation permanently in effect on that element, and it keeps overriding those properties for the element's whole life. For a one-shot entrance that ends in the element's natural resting state, the cleaner pattern is to make the keyframes end *at* the declared value, so removing the fill changes nothing. For a state change that must persist and later be overridden by other rules — an element that ends up hidden, disabled, or collapsed — the more maintainable pattern is to listen for the `animationend` event and apply a class that sets the final state as a normal declaration. Then the state lives in the cascade, where the rest of your stylesheet can reason about it. ## Quick diagnosis "My animation flashes back at the end" → you need `forwards`, or keyframes that end at the resting value. "My delayed animation shows the final state while it waits" → you need `backwards`. "My class can't override the finished animation" → a `forwards` fill is still applied; drop the fill and apply an end-state class instead.
- With animation: slide 1s 2 alternate forwards, which keyframe's values persist after it finishes?The `0%` keyframe. `alternate` runs the first iteration forwards and the second backwards, so the final iteration ends where the animation started. `forwards` holds the last keyframe *encountered*, not the `100%` block. With an odd iteration count — `3 alternate` — it would hold `100%` instead.
- Why can a class you add after the animation ends fail to change the element?Because a `forwards` fill is still applied, and animated values sit above normal author declarations in the cascade. The new class's declarations lose even at higher specificity. Either give them `!important`, or drop the fill and set the end state as a normal declaration applied on `animationend`, so the value lives in the cascade instead.
- Does animation-fill-mode: forwards change the element's inline style?No. It contributes an animated value in the cascade, nothing more. `element.style` stays empty and the declared rules are unchanged; only `getComputedStyle` reflects the filled value. Script that expects to read the final state off the inline style will find nothing there.
saying these in an interview costs you the question
- Believing forwards always holds the 100% keyframe
- Thinking fill-mode affects the animation's own timing
- Saying backwards plays the animation in reverse
- Assuming a forwards fill writes an inline style
- Expecting the last keyframe to stick by default