14

See this animation:

  • The golden div has an animation where a custom property is animated
    (@keyframes roll-o-1 animates --o).
    This animates in steps.
  • The silver div has an animation where a normal property is animated
    (@keyframes roll-o-2 animates left).
    This animates continuously.

Why doesn't the golden div animate smoothly?
Is there any workaround which also uses variables?

#one {
  width: 50px;
  height: 50px;
  background-color: gold;
  --o: 0;
  animation: roll-o-1 2s infinite alternate ease-in-out both;
  position: relative;
  left: calc(var(--o) * 1px);
}

@keyframes roll-o-1 {
  0% {
    --o: 0;
  }
  50% {
    --o: 50;
  }
  100% {
    --o: 100;
  }
}

#two {
  width: 50px;
  height: 50px;
  background-color: silver;
  --o: 0;
  animation: roll-o-2 2s infinite alternate ease-in-out both;
  position: relative;
}

@keyframes roll-o-2 {
  0% {
    left: 0px;
  }
  50% {
    left: 50px;
  }
  100% {
    left: 100px;
  }
}
<div id="one"></div>
<br>
<div id="two"></div>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
yunzen
  • 32,854
  • 11
  • 73
  • 106
  • Does this answer your question? [CSS animate custom properties/variables](https://stackoverflow.com/questions/50661638/css-animate-custom-properties-variables) – Mahozad Oct 20 '21 at 09:18

5 Answers5

19

When this question was asked, it wasn't possible to animate custom properties, as @temani afif correctly pointed out -

since the UA has no way to interpret their contents

Since then, CSS Houdini have put together the CSS Properties and Values API specification

This specification extends [css-variables], allowing the registration of properties that have a value type, an initial value, and a defined inheritance behaviour, via two methods:

A JS API, the registerProperty() method

A CSS at-rule, the @property rule

So now that you can register your own custom properties - including the type of the custom property - animating the custom property becomes possible.

To register the custom property via CSS - use the @property rule

@property --o {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

#one {
  width: 50px;
  height: 50px;
  background-color: gold;
  --o: 0;
  animation: roll-o-1 2s infinite alternate ease-in-out both;
  position: relative;
  left: calc(var(--o) * 1px);
}

@keyframes roll-o-1 {
  0% {
    --o: 0;
  }
  50% {
    --o: 50;
  }
  100% {
    --o: 100;
  }
}

#two {
  width: 50px;
  height: 50px;
  background-color: silver;
  animation: roll-o-2 2s infinite alternate ease-in-out both;
  position: relative;
}

@keyframes roll-o-2 {
  0% {
    left: 0px;
  }
  50% {
    left: 50px;
  }
  100% {
    left: 100px;
  }
}

@property --o {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
<div id="one"></div>
<br>
<div id="two"></div>

To register the property via javascript - use the CSS.registerProperty() method:

CSS.registerProperty({
      name: "--o",
      syntax: "<number>",
      initialValue: 0,
      inherits: "false"
   });

CSS.registerProperty({
  name: "--o",
  syntax: "<number>",
  initialValue: 0,
  inherits: "false"
});
#one {
  width: 50px;
  height: 50px;
  background-color: gold;
  --o: 0;
  animation: roll-o-1 2s infinite alternate ease-in-out both;
  position: relative;
  left: calc(var(--o) * 1px);
}

@keyframes roll-o-1 {
  0% {
    --o: 0;
  }
  50% {
    --o: 50;
  }
  100% {
    --o: 100;
  }
}

#two {
  width: 50px;
  height: 50px;
  background-color: silver;
  animation: roll-o-2 2s infinite alternate ease-in-out both;
  position: relative;
}

@keyframes roll-o-2 {
  0% {
    left: 0px;
  }
  50% {
    left: 50px;
  }
  100% {
    left: 100px;
  }
}
<div id="one"></div>
<br>
<div id="two"></div>

NB

Browser support is currently limited to chrome (v78+ for registerProperty(), v85+ for @property) edge and opera

Danield
  • 121,619
  • 37
  • 226
  • 255
  • Thanks for the input. I figured it out myself in the meantime without paying much attention to your input. Shame on me. I will set this as the accepted answer, if browser support will be better in the future. – yunzen Jan 08 '21 at 09:00
  • Alas, this is not production ready. https://caniuse.com/?search=%40property – yunzen Jul 07 '22 at 09:42
11

From the specification:

Animatable: no

Then

Notably, they can even be transitioned or animated, but since the UA has no way to interpret their contents, they always use the "flips at 50%" behavior that is used for any other pair of values that can’t be intelligently interpolated. However, any custom property used in a @keyframes rule becomes animation-tainted, which affects how it is treated when referred to via the var() function in an animation property.

So basically, you can have transition and animation on property where their value are defined with a custom property but you cannot do it for the custom property.

Notice the difference in the below examples where we may think that both animation are the same but no. The browser know how to animate left but not how to animate the custom property used by left (that can also be used anywhere)

#one {
  width: 50px;
  height: 50px;
  background-color: gold;
  animation: roll-o-1 2s infinite alternate ease-in-out both;
  position: relative;
  left: calc(var(--o) * 1px);
}

@keyframes roll-o-1 {
  0% {
    --o: 0;
  }
  50% {
    --o: 50;
  }
  100% {
    --o: 100;
  }
}

#two {
  width: 50px;
  height: 50px;
  background-color: silver;
  --o: 1;
  animation: roll-o-2 2s infinite alternate ease-in-out both;
  position: relative;
}

@keyframes roll-o-2 {
  0% {
    left: calc(var(--o) * 1px);
  }
  50% {
    left: calc(var(--o) * 50px);
  }
  100% {
    left: calc(var(--o) * 100px);
  }
}
<div id="one"></div>
<br>
<div id="two"></div>

Another example using transition:

.box {
  --c:red;
  background:var(--c);
  height:200px;
  transition:1s;
}
.box:hover {
  --c:blue;
}
<div class="box"></div>

We have a transition but not for the custom property. It's for the backgroud because in the :hover state we are evaluating the value again thus the background will change and the transition will happen.


For the animation, even if you define the left property within the keyframes, you won't have an animation:

#one {
  width: 50px;
  height: 50px;
  background-color: gold;
  animation: roll-o-1 2s infinite alternate ease-in-out both;
  position: relative;
  left: calc(var(--o) * 1px);
}

@keyframes roll-o-1 {
  0% {
    --o: 0;
     left: calc(var(--o) * 1px);
  }
  50% {
    --o: 50;
     left: calc(var(--o) * 1px);
  }
  100% {
    --o: 100;
    left: calc(var(--o) * 1px);
  }
}

#two {
  width: 50px;
  height: 50px;
  background-color: silver;
  --o: 1;
  animation: roll-o-2 2s infinite alternate ease-in-out both;
  position: relative;
}

@keyframes roll-o-2 {
  0% {
    left: calc(var(--o) * 1px);
  }
  50% {
    left: calc(var(--o) * 50px);
  }
  100% {
    left: calc(var(--o) * 100px);
  }
}
<div id="one"></div>
<br>
<div id="two"></div>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • 2
    To be clear: In your transition example, you should differentiate between `transition: background 1s;` (working) and `transition: --c 1s;` (not working). – yunzen Feb 11 '19 at 08:30
  • @yunzen exactly, but I didn't want to use the last syntax because it's not a common one and we may probably never write it this way. I wanted to focus on the fact that *we may think* it's a transition of the custom property but in reality it's for the background. – Temani Afif Feb 11 '19 at 08:43
  • 1
    @TemaniAfif do you happen to know the reasoning behind this decision in specification? In my blissful ignorance, I don't see much difference between computing let's say `{from{opacity:0}to{opacity:1}}` and `{from{--prop:0}to{--prop:1}}`, both are just properties, both are numbers, so one would expect both could be equally "tweenable"… – myf Oct 17 '19 at 22:02
  • @myf mainly for two reasons (1) a custom property can also contain character, spaces, etc and not only numbers and a string cannot be animated thus it would be tedious to have a definition that says only animate it if it's defined as a number (2) custom property can be applied to non-animatable properties and we have no idea to which property it will be applied until we resolve the CSSOM. In other words, there is a lot of cases where we cannot handle transition/animation and the easiest way is to make it non-animatable – Temani Afif Oct 17 '19 at 22:10
  • @TemaniAfif I don't see why aniamting only if a custom property is a number, is tedious. It's a simple conditional statement. In your second point, who cares? If we animate a number, then set it on another property that doesn't work with numbers, then I'd expect it not to work (but the custom property still be animated). – trusktr May 02 '20 at 00:28
  • @trusktr I didn't say it's tedious to animate a number. I said it's tedious to define a spec to say make some testing and if it's a number animate it. it's seems easy but it's not. A simple example: I want to animate from 1 to 10 how would you handle it? as integer (1,2,3,4, .. ,10) as float (1,1.005,1.006,...), you will define a step? the user can define a step? how to define it, etc etc ... there is a lot to consider than a simple if statement. Maybe in the next iteration of CSS variable the Spec will become more complex and we will be able to have such animation. – Temani Afif May 02 '20 at 00:46
  • @TemaniAfif I think it would animate as float values. Are there any CSS number properties that animate with discrete values instead of floats (f.e. when on an easing curve)?I agree, modifying specs is time consuming. – trusktr May 11 '20 at 21:51
  • @trusktr yes there is a lot like number of columns for example (`columns:3`) or the repeat inside CSS grid (`repeat(4,1fr)`), etc that's why it's not easy to handle all the cases and decide how to do the interpolation. Let's not forget the fact that some property accept negative values and other doesn't and probably more complex cases. – Temani Afif May 11 '20 at 21:54
  • So if we wrote `columns: var(--custom)` and animated `--custom`'s value, what should be the result? Should it just be the same as placing any incorrect value for `columns`? With the new `CSS.registerProperty`, we can define animatable custom properties. What happens if we place those on `column`? Should the result then be just the same when any property is placed on `column` (and it doesn't work) regardless if we animate it or not? – trusktr May 12 '20 at 04:03
4

Not all CSS properties are animatable, and you cannot animate css variables. This is the list of the properties you can animate https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties

arieljuod
  • 15,460
  • 2
  • 25
  • 36
  • 1
    But don't you think, they should be animatable? – yunzen Feb 08 '19 at 14:18
  • @yunzen it would be great I guess, but they just can't be animated, the CSS documentation states that, I don't know what to tell you. Maybe there's some proposed feature for the next CSS specifications, I don't know, right now you can't (the workaround is to animate the value using JS) – arieljuod Feb 08 '19 at 14:21
  • @yunzen It unreasonable that they can't be animated (under the constraint of the values being numbers). – trusktr May 02 '20 at 00:30
3

I can do this with the new CSS Properties and Values API Level 1
(part of CSS Houdini; W3C Working Draft, as of 13 October 2020)

I only need to register my custom property with the @property rule

    @property --o {
      syntax: "<number>";
      inherits: true;
      initial-value: 0;
    }

Via the syntax property I declare this custom property to be of type <number>, which hints the Browser in which way the calculations for transitioning or animating of this property should take place.

Supported values for the syntax property are listed here

"<length>"
"<percentage>"
"<length-percentage>"
"<color>"
"<image>"
"<url>"
"<integer>"
"<angle>"
"<time>"
"<resolution>"
"<transform-function>"
"<custom-ident>"

Browser compatibility is surprisingly strong, since this is an experimental feature and in draft status (See caniuse also). Chrome and Edge support it, Firefox and Safari don't.

<edit> Safari supports it since 16.4. Firefox will support it in the release 118, which is the current nightly build and will be released in late september 2023: </edit>

@property --o {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

#one {
  width: 50px;
  height: 50px;
  background-color: gold;
  --o: 0;
  animation: roll-o-1 2s infinite alternate ease-in-out both;
  position: relative;
  left: calc(var(--o) * 1px);
}

@keyframes roll-o-1 {
  0% {
    --o: 0;
  }
  50% {
    --o: 50;
  }
  100% {
    --o: 100;
  }
}

#two {
  width: 50px;
  height: 50px;
  background-color: silver;
  --o: 0;
  animation: roll-o-2 2s infinite alternate ease-in-out both;
  position: relative;
}

@keyframes roll-o-2 {
  0% {
    left: 0px;
  }
  50% {
    left: 50px;
  }
  100% {
    left: 100px;
  }
}
<div id="one"></div>
<br>
<div id="two"></div>
yunzen
  • 32,854
  • 11
  • 73
  • 106
0

Maybe not the answer you're looking for, but I achieved this using javascript animation (fx with gsap)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body,html {
            height: 100%;
            display: flex;
        }

        .wrapper {
            margin: auto 0;
        }

        .box {
            --animate:0;
            background-color: tomato;
            height: 70px;
            width: 70px;
            transform: translateX(calc(var(--animate) * 1px)) rotate(calc(var(--animate) * 1deg));
        }
    </style>
</head>
<body>
    
    <div class="wrapper">
        <div class="box"></div>
        <button onclick="play()">Play</button>
    </div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js" integrity="sha512-H6cPm97FAsgIKmlBA4s774vqoN24V5gSQL4yBTDOY2su2DeXZVhQPxFK4P6GPdnZqM9fg1G3cMv5wD7e6cFLZQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script>
        const tween = gsap.to(".box",{
            "--animate":900,   
            duration:10
        })

        tween.pause();

        function play() {
            tween.progress(0);
            tween.play();
        }
    </script>
</body>
</html>
Boris Grunwald
  • 2,602
  • 3
  • 20
  • 36
  • 1
    Thanks for your attempt, but I want to keep JS out of this (out of everything, actually) as most as possible. – yunzen Feb 08 '22 at 07:40