Custom properties and animations

20 January 2026

Custom properties, also known as variables, are one of my favourite features of CSS. Getting started with them is easy and intuitive, but they also allow to create very powerful and complex systems. One of their qualities is that they can contain anything.

/* This is fine */
--my-color: red;
/* This is fine too */
--my-number: 1;
/* Still fine */
--my-grid-template: [start] 1fr [middle] 1fr [end];
/* ...fine! */
--my-random-string: "random string of words";

In other words, they are untyped, they are just a general container.

When the browser parses a stylesheet, it just replaces the variable names with their value. It doesn’t do any additional check and it will work as long as the value is included in the property data-types. You can find all the CSS data-types listed here if you are curious.

.example {
  --my-color: red;
  background: var(--my-color);
  height: var(--my-color);
}

In the example above, background can receive a <color> data-type and red is a valid value, so it works fine. However, height doesn’t know what to do with it, since <color> is not included among its data-types.

The drawback of this untyped nature is that @keyframes don’t work well with variables and for years, we just accepted that they couldn’t be used in this scenario.

@keyframes rotate {
  from { --position: 0deg; }
  to { --position: 180deg; }
}

In order to animate something, browsers need to calculate all the steps between two values, and when we use variables in @keyframes, browsers don’t have any information on the type they are trying to parse and just bail. In reality the animation is running in the background, but the browser only displays the start and end values, without any interpolated values.

div {
  height: 15rem;
  aspect-ratio: 1;
  border-radius: 50%;
  background-image: conic-gradient(from var(--position), peachpuff, crimson);
  animation: rotate 4s linear infinite;
}

@keyframes rotate {
  from { --position: 0deg; }
  to { --position: 180deg; }
}

Fortunately, this problem was addressed with the introduction of @property as part of CSS Hudini.

All major browsers support it since 2024.

@property allows us to specify the type of a variable together with a few other things such as the initial value. By doing so, we can instruct the browser how to parse the value and enable it to calculate all the steps required in the animation.

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

Just adding these few lines to the previous example fixes the animation.

All three descriptors above are required for the custom property to work properly.

The syntax is the type we want the custom property to be. There are many different syntax values to choose from and you can even combine them.

@property --size {
  syntax: '<lenght> | <percentage>';
  initial-value: 0%;
  inherits: true;  
}

The initial-value is pretty self descriptive and can be omitted if you allow every type using syntax: '*', but why would you?

The inherits boolean decides if the property value can be inherited by a child component.

@property --my-background {
  syntax: '<color>';
  inherits: true;  
  initial-value: blue;
}

.parent {
  --theme-color: red;
}

/* if inherits is true, the child will be red */
/* if inherits is false, the child will be blue */
.child {
  background-color: var(--my-background);
}

One thing to keep in mind is that variables defined with @property are always global, and cannot be scoped to selectors.

So here it is, a new feature that makes your custom properties even more powerful than before, enjoy!