Animate to Different End States Using One Set of CSS Keyframes

Avatar of Ana Tudor
Ana Tudor on (Updated on )

I have recently live coded a pure CSS random rainbow particle explosion. There’s a source in the middle of the screen, and rainbow particles shoot out with different speeds at different moments and then fade out. It might seem like the kind of thing that requires a lot of work and code, but it’s something I did quite quickly and with only 30 lines of SCSS.

GIF of the particle explosion animation.

This writeup is about showcasing one particularly cool trick: there’s just one set of keyframes used to move all those particles on the screen! There’s a second one that’s responsible for fading them. But, even though they end up in completely different positions on the screen, only one set of keyframes is in charge of that.

Let’s see how it all works!

Structure

There’s not much going on here. We just drop 400 particles into the body element. I used Haml because I feel it provides the simplest way of looping that doesn’t even involve a loop variable I’m not going to need anyway. Note that it’s all down to personal preference. I tend to go with the preprocessor that gives me the result I want with the least amount of code because the less I need to write, the better. In this particular case, it happens to be Haml. But, at the end of the day, any preprocessor that lets you generate all the .particle elements in a loop works just fine.

- 400.times do
  .particle

Basic styles

The first thing we do is size our particles and absolutely position. I chose to make them 4x4 squares because I was going for a pixelated look.

$d: 4px;

.particle {
  position: absolute;
  width: $d; height: $d;
}

Random positioning on the screen

We loop through these 400 particles (note that the number in the @for loop in the SCSS needs to be the same number as the one we’ve used in the Haml loop) and we translate them at random x,y points on the screen, where x is between 1vw and 100vw and y is between 1vh and 100vh. random(100) gives us random integers between 1 and 100, both inclusive.

At the same time, we also give these particles different random backgrounds so we can see them. We pick the hsl() format because it’s the most convenient one here. random(360) covers the entire hue wheel giving us random values between 1 and 360.

Hue scale from 0 to 360.

We then max out the saturation (100%) and set for a lightness above 50% (65% in this case).

.particle {
  /* same styles as before */

  @for $i from 0 to 400 {
    &:nth-child(#{$i + 1}) {
      transform: translate(random(100)*1vw, random(100)*1vh);
      background: hsl(random(360), 100%, 65%);
    }
  }
}

We can now see our particles distributed all over the screen:

See the Pen by thebabydino (@thebabydino) on CodePen.

They trigger scrollbars, so we set overflow: hidden on the root element. We also give it a dark background so we can see our light rainbow particles better:

html {
  overflow: hidden;
  background: #222;
}

This way, we get a pretty night sky:

See the Pen by thebabydino (@thebabydino) on CodePen.

Animation

The next step is to animate these particles, making them shoot out from the middle of the screen. This means our animation starts at the 50vw,50vh point:

@keyframes shoot {
  0% { transform: translate(50vw, 50vh); }
}

We don’t specify a final (100%) keyframe. If this is isn’t specified, it gets automatically generated from the styles we have set on the elements we animate – in our case, these are the random translates, different for each and every particle.

We want the motion of the particles to be fast at first and then slow down, which means we need to use an ease out type of timing function. We can just go for the plain old ease-out, or we can use a more advanced one (I go to easings.net for this). We then give our animation a dummy duration of 3s and make it repeat infinitely.

.particle {
  /* same styles as before */
  animation: shoot 3s ease-out infinite;
}

We get the following result:

See the Pen by thebabydino (@thebabydino) on CodePen.

The particles shoot out to different positions in the plane, just like we wanted. But they all animate at once, which isn’t what we want. So the first fix is to give each one of them a different, random animation duration between 1s and 3s within the loop:

.particle {
  /* same styles as before */

    @for $i from 0 to 400 {
        $t: (1 + .01*random(200))*1s;

        &:nth-child(#{$i + 1}) {
            /* same styles as before */
            animation-duration: $t;
        }
    }
}

This is much better:

See the Pen by thebabydino (@thebabydino) on CodePen.

Then we give each particle a random negative delay between 0% and 100% of its animation-duration ($t) in absolute value:

.particle {
  /* same styles as before */

    @for $i from 0 to 400 {
        $t: (1 + .01*random(200))*1s;

        &:nth-child(#{$i + 1}) {
            /* same styles as before */
            animation-delay: -.01*random(100)*$t;
        }
    }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Finally, we don’t want the particles to just disappear, so we add a second animation (with the same duration and the same delay) to fade them out:

.particle {
  /* same styles as before */
  animation: shoot 0s ease-out infinite;
  animation-name: shoot, fade;
}

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

We now have the final result!

See the Pen by thebabydino (@thebabydino) on CodePen.