Partial Keyframes • Josh W. Comeau

, but that isn’t a requirement for this pattern. It’ll still work if the resting opacity is set with a CSS class, or however else you typically apply CSS!

And if an element doesn’t set the opacity property at all, like that first .ball in the playground, the default value of 1 will be used. This is awesome, since it means that our fadeToTransparent keyframe is just as easy-to-use as a traditional fadeOut animation. We don’t have to explicitly set opacity: 1 in order to fade stuff out with this technique!

The same trick works in the other direction. If you omit the to value, the animation will animate from a specified value to whatever value it’s currently set to:

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

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

In this demo, we animate from 0 to whatever the element’s specified opacity is. If the element doesn’t have an explicit opacity set, it will default to 1, acting like a regular fade-in animation.

What’s the use case for this? This trick is handy when working with elements that aren’t fully opaque by default, or have state-based opacity. Here’s a real-world example:

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

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

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

This button has a resting opacity of 0.7, but when the user hovers over it, the opacity flips to 1. This is a useful UX pattern because it helps convey that this element is interactive.This is known as “affordance” in UX design circles. This button already has pretty good affordance, since the cursor flips to a pointer on hover, but I find the experience is even better with an additional indication.

(I’ve also set it up here to rise to full opacity on focus and tap, so that you can experience this even if you’re not using a mouse, but in practice I think it’s fine for this particular detail to be mouse-only.)

This button also fades in on mount, and so I’m using the partial keyframes trick to ensure that it always fades to the correct value. If the user happens to be hovering over it when it mounts, it’ll fade to 1. Otherwise, it’ll fade to 0.7. ✨

This next bit really blew my mind when I discovered it. With partial keyframes, we can animate to a value specified by another keyframe animation!

Check this out:

<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>

Let me explain what’s going on here:

  • The twinkle keyframe animation causes the ball to oscillate between 0.25 and 0.75 opacity. It bounces back and forth thanks to the alternate keyword, and runs forever thanks to infinite.

  • Our fadeFromTransparent keyframe sets an initial opacity of 0, but doesn’t specify the target opacity.

  • When combined, we fade from 0 to the ever-changing value set within twinkle. It essentially allows us to gradually introduce the flickering twinkle animation.

Let’s graph the opacity changes over time, so that we can really see what’s going on here. Toggle between the two values to see the effect of stacking these keyframe animations:

Animation:

Pretty wild, right?? Multiple keyframe animations can modify the same property without one cancelling the other!

If you found this tutorial useful, I have some good news for you!

Over the past 18 months, I’ve been working on a new animations course. This lil’ trick was plucked straight from the course, and it’s just the tip of the iceberg. I’ll show you all of the techniques and strategies I use to design and build polished, next-level animations using CSS, JavaScript, SVG, and Canvas.

Whimsical Animations, a course from Josh W. Comeau

If you’ve ever wondered how I created some animation on this blog, there’s a very good chance we cover it in the course 😄. Check it out:

Link to this headingBonus: dynamic values in keyframe definitions

There’s one more trick I want to share with you. 😄

Let’s suppose we’re building the following tail-wagging animation:

For this sort of endless motion, CSS Keyframe animations are the best tool in the toolbox. But hm, keyframe animations require that we set specific, hardcoded values.

So, if we only had one ball, this wouldn’t be a big deal. We could set it up like this:

@keyframes oscillate {
  from {
    transform: translateX(-16px);
  }
  to {
    transform: translateX(16px);
  }
}

.ball {
  animation: oscillate 1000ms infinite alternate;
}

But, we have four balls, and they each oscillate by a different amount. 🤔

For years, this was a thorn in my side. I had to either create four nearly-identical keyframe animations, each with a different hardcoded value, or I would wire it up using CSS transitions and JavaScript intervals. Both options were thoroughly unsatisfying.

And then, I had a mindblowing realization.

Check this out:

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

Instead of hardcoding a specific value like 16px inside our keyframe definition, we can access a CSS variable! With a little help from calc, we can flip that value to its negative counterpart, so that we can oscillate to/from a dynamic value.

In order for this to work, we need to define an --amount value on each element that is being animated. For example, we could do that with 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>

How freaking cool is this?!

When I first discovered this trick, it kinda blew my mind. I had no idea we could read CSS variables from within a keyframe animation! This was the final puzzle piece that fully unlocked keyframe animations for me, making them just as dynamic and flexible as CSS transitions.

Here’s a full editable demo that showcases this technique:

Code Playground

<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>


<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 animationbutton>

In terms of browser support, I believe that this has been supported since CSS variables themselves were introduced. According to caniuse, support is sitting around 96%(opens in new tab).

CSS keyframe animations can be pretty confusing, so if you’re not sure what keywords like alternate are doing, you can check out my interactive blog post on the subject.

And if you’d like to learn about whimsical animation more broadly, don’t forget to sign up for updates about my course!(opens in new tab)

Last updated on

May 5th, 2026

Similar Posts

Leave a Reply