Nate Moore

Published November 22, 2019

Controlling complex animations with custom properties

I recently added a new trick to my CSS toolbelt, and I've found myself using it all over the place.

Using the power of --custom-properties and some interesting behavior of animation, here's a very clever way to scrub through a CSS animation in response to user interaction. đź’ˇ

Credit where credit is due— I first saw this trick in use on the Tornis site by Robb Owen. Scott Kellum created the technique.

I’m just spreading the word.


Here’s the secret—animation-delay accepts negative values. A negative value begins the animation immediately (like animation-delay: 0), but starts playing partway through its cycle.

By combining this trick with animation-play-state: paused, we can control the animation directly with a custom property.

This trick is incredibly useful for complex hover animations, coordinating color changes across elements, or proximity feedback.

.my-element {
  /* Setup */
  animation-name: spin;
  animation-timing-function: linear;

  /* Here's the magic */
  animation-play-state: paused;
  animation-duration: 1s;
  animation-delay: calc(var(--progress) * -1s);

  /* These clean up some weirdness */
  animation-iteration-count: 1;
  animation-fill-mode: both;
}

If you’re looking for a one-liner, try this animation shorthand.

.my-element {
  /* Binds `spin` animation to var(--progress) */
  animation: spin 1s calc(var(--progress) * -1s) paused linear 1 both;
}

To keep it even simpler, you could use a --bind-animation “mixin” (by the way, is Houdini ready yet?)

:root {
  --bind-animation: paused linear 1 both;
}
.my-element {
  animation: spin 1s calc(var(--progress) * -1s) var(--bind-animation);
}

Or a real Sass mixin.

@mixin scrub-animation-on($property) {
  animation: 1s calc(#{$property} * -1s) paused linear 1 both;
}