skip to content

In a CSS @keyframes rule, what supplies the starting value if you only declare a to stop, and what happens to a property that appears in some keyframes but not others?

level: middleimportance: should knowfreq 42%

answer

  1. missing stops are filled in
  2. from the element's own computed value
  3. interpolation is per property
  4. same name, later rule wins outright
  5. important inside keyframes is dropped

basics

~20 s

The browser synthesises the missing 0% or 100% stop from the element's own computed value for each animated property. A property declared in only some keyframes interpolates between the stops that do declare it, using that computed value as the implicit endpoint.

solid answer

~50 s

Keyframes do not have to be complete. If a `@keyframes` rule omits `0%` or `100%`, the browser constructs an implicit keyframe there from the element's underlying computed values for exactly the properties the rule animates — so `@keyframes grow { to { opacity: 1; } }` starts from whatever opacity the cascade gives the element. That makes animations reusable across elements with different resting states, and it is also why the same rule can look different on two elements. The rule applies per property, not per keyframe: if `opacity` is declared at `0%` and `100%` but `translate` only at `50%`, `translate` interpolates from the element's computed value up to the `50%` value and back down. Two blocks at the same offset cascade together, later declarations winning per property, and `!important` inside a keyframe is ignored entirely.

code

css · 10 lines
css
@keyframes fade-out { to { opacity: 0; } }

.full { opacity: 1;   animation: fade-out 300ms forwards; }
.dim  { opacity: 0.5; animation: fade-out 300ms forwards; }

@keyframes attention {
  0%   { opacity: 0; }
  50%  { scale: 1.2; }
  100% { opacity: 1; }
}

go deeper

for a junior

Know that from/to are aliases for 0%/100% and that you do not have to write both — the browser fills the missing end from the element's current value.

for a middle

Explain implicit endpoint keyframes and per-property interpolation, and predict what a rule that declares a property in only one middle keyframe will actually do.

for a senior

Bring the operational angle: implicit endpoints make one animation behave differently per element, duplicate @keyframes names silently replace each other, and per-segment easing is how multi-phase motion is authored.

for a principal

Own the naming and ownership rules for a shared motion layer, since @keyframes names are global with last-one-wins semantics and a collision across teams produces a bug with no error and no obvious cause.

## Keyframes are a sparse description A `@keyframes` rule is not a set of complete style snapshots. It is a sparse list of offsets, each declaring some properties, and the browser fills in everything else. Two mechanisms do that filling: implicit endpoint keyframes, and per-property interpolation between the stops that mention each property. ## The implicit 0% and 100% If the rule has no `0%` (or `from`) block, the browser creates one whose values are the element's **underlying computed values** for each property the animation touches. Same for a missing `100%` (or `to`). ```css @keyframes fade-out { to { opacity: 0; } } .a { opacity: 1; animation: fade-out 300ms; } /* fades from 1 */ .b { opacity: 0.5; animation: fade-out 300ms; } /* fades from 0.5 */ ``` One rule, two different animations, because the implicit `0%` came from each element. This is the reason single-stop keyframes are so common for utility animations — they adapt to the element instead of hard-coding a start. The flip side is a class of confusing bug: change the element's resting `opacity` in an unrelated rule and the animation silently changes shape. When the start value must be fixed, declare `from` explicitly. ## Per-property, not per-keyframe The interpolation is computed property by property. A property is animated between the keyframes that *declare* it, with the implicit endpoints filling in the rest: ```css @keyframes attention { 0% { opacity: 0; } 50% { scale: 1.2; } 100% { opacity: 1; } } ``` `opacity` runs `0 → 1` across the whole duration, passing through `0.5` at the halfway mark. `scale` is declared only at `50%`, so it interpolates from the element's computed scale to `1.2` over the first half, then back to the computed scale over the second half. Nothing about `opacity` constrains `scale`, and the `50%` block does not need to restate `opacity`. ## Same offset declared twice Blocks with the same offset do not replace each other wholesale — they cascade per property, later declarations winning: ```css @keyframes x { 50% { opacity: 0.5; color: red; } 50% { opacity: 0.2; } /* opacity 0.2, colour still red */ } ``` And one block may carry several offsets, which is the idiomatic way to write a symmetric animation: ```css @keyframes shake { 0%, 100% { translate: 0; } 25%, 75% { translate: -6px; } 50% { translate: 6px; } } ``` Offsets outside the `0%–100%` range are invalid and the block is dropped; offsets need not be written in order, since the browser sorts them. ## Two rules with the same name Unlike blocks inside one rule, entire `@keyframes` rules with the same name do **not** merge. The last one in document order wins completely, and the earlier ones are ignored — every offset, every property. This bites when a component stylesheet and a shared stylesheet both define `@keyframes fade`: whichever is loaded second is the only one that exists. ## !important is ignored A declaration marked `!important` inside a keyframe block is dropped. There is no way to raise an animated value's weight from inside the `@keyframes` rule; animated values already apply above normal author declarations, and `!important` author rules already outrank them. Writing `!important` in a keyframe is always either a no-op or a misunderstanding. ## Per-segment easing `animation-timing-function` declared inside a keyframe block applies to the segment that *starts* at that keyframe and runs to the next one — not to the whole animation. On the final `100%` block it has no segment to govern and is ignored. ```css @keyframes drop { 0% { translate: 0 -100px; animation-timing-function: ease-in; } 70% { translate: 0 0; animation-timing-function: ease-out; } 100% { translate: 0 -8px; } } ``` This is how bounce-like motion is written without a single cubic curve trying to express the whole path. ## What to check when a keyframe animation looks wrong Does the animation start from a value you did not expect? An implicit endpoint is picking up the element's computed value. Does one property jump instead of moving? It is probably declared in only one keyframe, so it is being interpolated against an implicit endpoint that happens to equal it. Did the whole rule vanish? Look for a second `@keyframes` with the same name later in the cascade.

  • Two stylesheets each define @keyframes fade with different steps. Which one runs?
    Only the one that comes last in document order. Unlike declarations, whole `@keyframes` rules with the same name never merge — the later rule replaces the earlier one completely, including offsets the earlier one had and it does not. Specificity is irrelevant here; there is no selector involved. Namespacing keyframe names per component avoids the collision entirely.
  • Where does animation-timing-function declared inside a keyframe block take effect?
    On the segment starting at that keyframe and ending at the next one. Each keyframe can therefore carry its own easing, which is how multi-phase motion like a bounce is written. A timing function on the final `100%` keyframe governs no segment and is ignored, and the value on the `animation` shorthand supplies the default for segments that do not set their own.
  • When should you declare an explicit from block rather than relying on the implicit one?
    Whenever the start value must be fixed regardless of the element. The implicit keyframe reads the element's underlying computed value, so an unrelated change to that element's resting styles silently changes the animation. Explicit `from` also makes the intent readable — a reviewer can see the whole path without going to look at the element's other rules.

saying these in an interview costs you the question

  • Assuming keyframes must declare 0% and 100%
  • Thinking every keyframe must list every property
  • Expecting two @keyframes with one name to merge
  • Using !important inside a keyframe block
  • Believing keyframe timing functions apply to the whole run

context