Partial Keyframes: The CSS Animation Trick That Adds Flexibility

Most CSS keyframe animations define both a starting and ending state using from and to. A standard fade-out might look like this:

@keyframes fadeOut {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

But what happens if you drop one of those blocks? For example, what if you only specify the ending point?

@keyframes fadeToTransparent {
  to {
    opacity: 0;
  }
}

It still works. The element fades out as expected even without an explicit starting opacity declared.

This is more than just a way to trim a few bytes from your stylesheets. Omitting a keyframe block allows the animation to pull its missing value from the element's current state. That single change makes keyframe animations dynamic and composable in ways that a fully-specified animation cannot be.

Where Missing Values Come From

When you leave out the from block, the animation takes its starting value directly from the element's computed style at the moment the animation begins.

Consider a group of elements, each with a different resting opacity set via inline style:

@keyframes fadeOut {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

A traditional fadeOut animation, which jumps to opacity: 1 first, works fine for the first yellow ball — the one that is fully opaque by default. For the others, the animation snaps them to full opacity before fading them back down, creating an unwanted flash.

A partial keyframe that specifies only the to state inherits each element's current opacity and starts the fade from there. No explicit opacity: 1 is needed even for elements that have no opacity set at all, since the default value of 1 is used in that case.

The full code for this comparison is available in the playground below:

<style>
  @keyframes fadeToTransparent {
    to {
      opacity: 0;
    }
  }

  .to-transparent {
    animation: fadeToTransparent 1000ms forwards;
  }
</style>

<div class="row">
  <div class="ball"></div>
  <div class="ball" style="opacity: 0.6"></div>
  <div class="ball" style="opacity: 0.3"></div>
</div>

<button>
  Toggle keyframe
</button>

This pattern works no matter how the resting opacity is applied — inline styles, CSS classes, or any other method.

Omitting the Destination

The same logic applies in the other direction. If you leave out the to block, the animation runs from the value you specify to whatever value the element currently has.

<style>
  @keyframes fadeFromTransparent {
    from {
      opacity: 0;
    }
  }

  .ball {
    animation: fadeFromTransparent 1000ms;
  }
</style>

This is useful for elements that are not fully opaque by default or that change opacity based on state. Consider a button with a resting opacity of 0.7 that rises to 1 on hover, focus, or tap — a common affordance for interactive elements.

<style>
  @keyframes fadeFromTransparent {
    from {
      opacity: 0;
    }
  }

  .icon-btn {
    opacity: 0.7;
    animation: fadeFromTransparent 1000ms;

    &:hover, &:active, &:focus-visible {
      opacity: 1;
    }
  }
</style>

If that button also fades in when it mounts, a partial keyframe that animates from transparent to the current value is the only way to ensure the fade lands on the correct state. If the user is already hovering when the button appears, it fades to 1; otherwise it settles at 0.7.

Animating to Another Animation's Value

Partial keyframes unlock an even more surprising capability: animating to a value that is itself being changed by a separate animation running on the same property.

<style>
  @keyframes twinkle {
    from {
      opacity: 0.25;
    }
    to {
      opacity: 0.75;
    }
  }

  @keyframes fadeFromTransparent {
    from {
      opacity: 0;
    }
  }

  .ball {
    animation:
      twinkle 250ms alternate infinite,
      fadeFromTransparent 2000ms;
  }
</style>

Here, two animations are applied to the same element:

  • A twinkle animation uses alternate and infinite to oscillate the ball's opacity between 0.25 and 0.75.

  • A fadeFromTransparent keyframe sets the starting opacity to 0 but includes no to block.

When combined, the element fades in from 0 to the ever-changing value produced by twinkle. The result is a smooth introduction of the flickering effect, and the two animations coexist without one overriding the other.

Using CSS Variables Inside Keyframes

A separate limitation of keyframe animations is that their values must be hardcoded. Animating a set of elements that each oscillate by a different amount traditionally meant writing a near-identical keyframe for each one, or falling back to JavaScript-driven transitions.

CSS variables solve that problem. Instead of hardcoding a value like 16px, a keyframe can read a custom property:

@keyframes oscillate {
  from {
    transform: translateX(calc(var(--amount) * -1));
  }
  to {
    transform: translateX(var(--amount));
  }
}

Supported with calc() to derive the negative counterpart, this allows the animation to oscillate between a variable-defined value and its inverse. Each element defines its own --amount, typically via an inline style:

<style>
  .ball {
    animation: oscillate 1000ms infinite alternate;
  }
</style>

<div class="ball" style="--amount: 8px"></div>
<div class="ball" style="--amount: 16px"></div>
<div class="ball" style="--amount: 32px"></div>
<div class="ball" style="--amount: 64px"></div>

This technique works for any property that accepts the value being animated. Browser support aligns with CSS variables themselves, which is around 96% according to caniuse.

Here is a fully editable demo showing the technique in action:

<style>
  @keyframes oscillate {
    from {
      transform: translateX(calc(var(--amount) * -1));
    }
    to {
      transform: translateX(var(--amount));
    }
  }

  .ball {
    animation: oscillate 700ms ease-in-out alternate infinite;
  }
</style>

<!-- Edit these values to change the oscillation amount: -->
<div class="ball" style="--amount: 8px"></div>
<div class="ball" style="--amount: 16px"></div>
<div class="ball" style="--amount: 32px"></div>
<div class="ball" style="--amount: 64px"></div>

<button>Play/pause animation</button>

With this pattern, keyframe animations gain the same kind of flexibility as CSS transitions — no need for JavaScript or repetitive keyframe definitions.