The gotcha with @property animating custom properties

With @property support being available in Chrome for a long time and now in Safari Technology preview too, it’s becoming really easy to use animated custom properties as the driver for a bunch of other things on your page. However, there’s one big gotcha with this: custom properties don’t animate on the compositor.

~

# @property 101

@property is an at-rule that allows you to register your CSS Custom Properties. You give them a certain type (syntax), an initial value, and can control whether they should inherit or not.

By registering a custom property to be of a certain type, the browser knows how to interpolate its values when used in transitions and animations.

@property --angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

@keyframes adjust-angle {
  to {
    --angle: 360deg;
  }
}

div {
  --angle: 0deg;
  animation: 10s adjust-angle linear infinite;
  rotate: var(--angle);
}

If you didn’t register --angle, the browser would not know its type and animate it discretely, meaning it would flip halfway the duration from 0deg to 360deg without any interpolation.

# More details and examples can be found on web.dev and in Exploring @property and its Animating Powers

~

# One CSS Custom Property to rule them all

⚠️ This demo relies on CSS features that are not supported by all browsers yet. Please use Chrome 111+ or Safari Technology Preview 162+. Firefox does not support @property at the time of writing.

Lets build a demo which animates two aspects of a box at the same time:

  • Rotate the box from 0deg to 360deg
  • Move the box down and up the y-axis over a distance of 100% on each side

Thanks to @property, combined with Individual Transform Properties and Trigonometric Functions, this becomes easy to do. Instead of animating the rotate and translate properties separately, you can animate a --angle custom property from 0deg to 360deg, and use its value in the rotate and translate properties.

@property --angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

@keyframes animate {
  to {
    --angle: 360deg;
  }
}

.box {
  animation: animate 5s linear infinite;
  transform-origin: 50% 50%;
  rotate: var(--angle);
  translate: 0 calc(sin(var(--angle)) * 100%);
}

As --angle constantly gets updated, so will the rotate and translate properties that depend on it.

See the Pen Animation in CSS, using a Custom Property by Bramus (@bramus) on CodePen.

For comparison, here is an alternative version that does not use a custom property

@keyframes animate {
  from {
    rotate: 0deg;
    translate: 0 0;
  }
  25% {
    translate: 0 100%;
  }
  50% {
    translate: 0 0;
  }
  75% {
    translate: 0 100%;
  }
  to {
    rotate: 360deg;
    translate: 0 0;
  }
}

.box {
  animation: animate 5s linear infinite;
  transform-origin: 50% 50%;
}

Visually, this code has the same outcome:

See the Pen Animation in CSS, not using a Custom Property by Bramus (@bramus) on CodePen.

Personally I find the first approach – the one using the --angle custom property – easier to grasp, build, and maintain.

This “One CSS Custom Property to rule them all”-approach is a common technique: by simply flipping a few switches you can have your layout respond to it. Take this demo by my colleague Jhey for example: only the --hue value changes, and all stripes of the rainbow respond to that change. Easy.

See the Pen Animated Custom Property by Jhey (@jh3y) on CodePen.

~

# The gotcha

Even though both box-demos both have the same visual outcome, the version that relies on --angle has a problem, as surfaced through a performance inspection:

Performance recording using Chrome DevTools
Timeline recording using Safari Web Inspector

While rotate and translate are typically properties that are animated on the compositor thread with the help of the GPU, this is not the case here: layout is constantly being trashed and it gets rasterized on every frame. Huh!?

Zooming in on the timeline, we see style constantly being invalidated, a successive style recalculation being triggered, and eventually a repaint being done.

Zoomed in timeline

Compare this to a trace of the demo that does not use the custom property to drive the animation.

Performance recording using Chrome DevTools
(version without Custom Property)

As the timeline shows, this version is silky smooth and does not need to constantly recalculate styles – it runs on the compositor, as one would have expected.

~

# But why?

So we dialed down the cause to the --angle custom property that’s being used to control the other properties. Digging into the specification, it becomes clear what goes on:

[T]he value of a registered custom property can be substituted into another value with the var() function. However, registered custom properties substitute as their computed value, rather than the original token sequence used to produce that value.

So as the animation runs, the value of the --angle custom property need to be updated as well. Because it gets passed as a computed value, style gets invalidated and a new value is computed. Once that’s done, the properties that rely on it require a repaint. Rinse and repeat.

# Note that it is not the use of @property but that it’s the use of custom properties in animations by itself that’s causing this. Style invalidation also constantly happens for keyframes with custom properties that are not registered using @property, as they also need to be computed at every tick. However, you wouldn’t typically use non-registered custom properties for your animations as these animate discretely, meaning they flip halfway the duration from one value to the other without any interpolation.

# What also is interesting here, is what happens when you disable the rotate and translate properties using DevTools. When doing so, repaint correctly stops from triggering but style invalidation still happens by simply having the animation run, even though --angle is not used anywhere.

~

# Can this be fixed?

If the compositor were able to do var-substitutions, I think this could be fixed. In the box example, the compositor would need to figure out a way to prevent the --angle custom property from causing a style invalidation while being animated, thereby preventing everything that follows.

Asking Chromium engineer Rune Lillesveen (futhark), he mentioned that it would require a somewhat deep understanding of such var() substitutions on the compositor – It doesn’t seem to be impossible, but definitely would require a substantial amount of work.

At the time of writing, this optimisation might seem unnecessary, but I guess that’s because @property usage today is low. My prediction is that this need will become more urgent, once Safari and Firefox ship @property as well, and people start actively relying on this.

There’s no real solution right now other than rewriting your animations to not rely on the registered custom property, which is a pity as its one of their biggest use cases. If you want to see this change, go star Chromium issue #1411864 to signal interest.

~

# tl;dr

Custom properties don’t animate on the compositor.

~

# Spread the word

To help spread the contents of this post, feel free to retweet its announcement tweet:

~

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.