Browser support for @property nears completion, a great time to delve into some technical aspects of this CSS feature. While I've experimented with @property in demo settings before, I’ve never used it in a production environment. With the upcoming full browser support, let’s take a look at the inner workings and uncover some of the benefits.

Without going too much into the history about this technique of handling our custom properties, it might be nice to repeat just a little of the basics for those landing here and having absolutely no clue what I’m going on about.

What is this @property thing?

The @property rule is part of the CSS Houdini umbrella of APIs, which aims to provide more control and flexibility for authors. It was introduced in the CSS Properties and Values API Level 1 spec around 2019 and has had a long support in Chrome and Edge, followed up by Safari. I personally didn’t experiment that much with Houdini when it first came around as it never felt natural to me.

The Purpose of @property

The @property rule allows developers to explicitly define custom CSS properties (also known as CSS variables). This provides several benefits:

  • Property type checking and constraining: We can specify the data type of a custom property, such as <number>, <color>, or <length>. This helps prevent errors and ensures that custom properties are used correctly, a win for design systems and we all love typing these days, right? Find the full list of syntax types here
  • Setting default values: We can set a default value for a custom property declared this way with the initial-value property. This ensures that the property has a fallback when not set elsewhere in the stylesheet.
  • Defining inheritance behavior: We get to choose whether a custom property can inherit values from its parent elements or not. That way we can manage how our custom properties are applied in the cascade, more about that later on.

Basics of @property

The @property rule gives us a more structured and powerful way to create and use custom properties. This opens up new possibilities for customization and especially theming. Here’s an example of how to define a custom property named --primary-color with a initial-value of #ea1ca5:

@property --primary-color {
    syntax: "<color>";
    inherits: true;
    initial-value: #ea1ca5;
}

This definition specifies that --primary-color is a color property, it can inherit values from parent elements, and its default value is #ea1ca5.

Once a custom property is defined, it can be used in CSS declarations just like any other property:

body {
  background-color: var(--primary-color);
}

This declaration sets the background-color of the body element to the value of the --primary-color custom property.

Now that we got the basics right, off to the fun stuff! Let’s talk about the opportunities this opens.

Property type checking and constraining

By using @property to define custom properties with clear types, we can easily maintain and update our code. This can make it less time-consuming and error-safe to modify custom properties as a project evolves. It’s even a bigger win when building a design system that gets used by third-parties, showing the users that something doesn’t seem right in DevTools.

Just as the first example, let create a little playground with color:

At the top of our CSS we will add the following:

@property --color {
    syntax: "<color>";
    inherits: true;
    initial-value: #ea1ca5;
}

Our playground HTML will contain three div elements, each with a different class:

<div class="color-1"></div>
<div class="color-2"></div>
<div class="color-3"></div>

Now let’s set the default custom property on the <div> element, give it some width and height. Next up, we’ll overwrite this custom property for each of the classes:

div {
    display: flex;
    justify-content: center;
    width: 20vw;
    height: 100px;
    background: var(--color);
}

.color-1 {
    --color: #1a535c;
}

.color-2 {
    --color: rgb(100, 200, 0);
}

.color-3 {
    --color: #3e47db;
}

With a bit of extra styling, this is the result.

The colors are all shown perfectly, no matter the color function

What happened here?

Notice that it doesn’t really matter how we write the colors when using the <color> type. For example, in .color-2, we chose to use the rgb() function and it still works.

So all of this is valid, but let’s take a look at what happens when we change .color-1 to an integer.

.color-1 {
    --color: 4;
}
Because of the wrong syntax for a color, the fallback is shown in color-1

We are now seeing our fallback color as the value is invalid. It’s handy to have a fallback system like this and personally, I love this.

Debugging @property in Chrome DevTools

With Chrome version 118, DevTools provides useful information about typed custom properties. Let’s inspect the element with an invalid color value after setting the --color property to 4. A warning icon appears next to the custom property. Hovering over the icon reveals an error message stating that the property value is invalid and the expected type is <color>.

DevTools in Chrome CSS panel showing notice: Invalid property value, expected type <color>

An anchor below the warning directs us to the custom property declaration.

DevTools in Chrome CSS panel showing the full @property information

When hovering over a valid custom property, we receive detailed information about the property:

  • The current value
  • The expected syntax
  • The inherits boolean
  • The initial value
  • And an anchor to the declaration
DevTools in Chrome, hovering over a custom property shows the current value and registered property info

This DevTools functionality significantly enhances the efficiency of using typed custom properties.

See it in action in this video by the DevTools team

Defining inheritance behavior

If the inherits property is set to true, the custom property value can be inherited by child elements. This means that if we set a value for the custom property on a parent element, that value will be applied to all of its child elements unless they have their own value set for the property. This is also the default for custom properties.

Let’s keep using our colors demo for this and add a new element inside of the div with class .color-2. This is now the updated HTML:

<div class="color-1"></div>
<div class="color-2">
    <div class="inner"></div>
</div>
<div class="color-3"></div>

Now let’s give this new div.inner a bit of styling. To see our element clearly, we’re going to give this a white border as well.

 .inner {
    background: var(--color);
    border: 3px solid white;
    width: 50px;
    aspect-ratio: 1;
}

We are making it inherit the --color custom property for its background, this will result to the following:

The inner div shows the same color as its parent, because inherit is set to true

Now let’s update our declaration of the custom property and set the inherits to false.

@property --color {
    syntax: "<color>";
    inherits: false;
    initial-value: #ea1ca5;
}

The result:

The inner div shows the fallback / initial-value color, because inherit is set to false

Because the inherits property is set to false, the custom property value cannot be inherited by child elements. This means that each child element must explicitly set its own value for the property, thus in this case, showing the initial-value color.

Why set inherits to false?

Disabling inheritance for custom properties could be a good practice when we need to have more control over the styling of individual elements and avoid unintended overrides (for example, in design systems or web components). It can also simplify our CSS code and make it more maintainable. However, if we would want to maintain a consistent style across a hierarchy of elements, inheritance can be a useful tool. This why I believe that setting inheritance to false should come with a sense of caution to avoid confusion when working on a large project with multiple authors. But that’s just my 2 cents, and writing documentation can go a long way.

Unlocking new possibilities with @property

The introduction of @property opened up exciting possibilities for CSS, particularly in the realm of animations. One of the most popular demonstrations of @property is the animation of a hue rotation wheel.

Consider the following example:

:root {
    --hue: 0deg;
}

.item {
    background: hsl(var(--hue), 50%, 50%)
}

@keyframes {
	from {
        --hue: 0deg;
    }
    to {
        --hue: 360deg;
    }
}

This won’t work at all, because the browser has no idea on how to animate hue rotation. However, using @property, we can explicitly instruct the browser to interpret the --hue variable as an <angle> value, enabling smooth animation:

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

Here is another example using that same technique in combination with some SVG:

Animating clip-path with custom properties

For a scroll driven animations demo I wanted a dotted line to appear in a timeline. To initially hide that line, animating a clip-path seemed like good idea, but we can’t just animate the percentages from a clip-path with a custom property. @property however, can be used to animate the dimensions of a clipping mask. For instance, to gradually reveal an element, we can define variables for the vertical and horizontal percentages of the clipping mask and animate them.

@property --clip-vertical {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 95%;
}

@property --clip-horizontal {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}

.item {
  clip-path: inset(0 0 var(--clip-vertical) var(--clip-horizontal));
}

We can set the initial value to (almost) completely hide the element with a full clip-path. To animate the removal of the clip-path, we can create an animation:

.item {
    clip-path: inset(0 0 var(--clip-vertical) var(--clip-horizontal));
    animation: showLine linear both;
}

@keyframes showLine {
  0% {
    --clip-horizontal: 100%;
    --clip-vertical: 95%;
  }
  60% {
    --clip-horizontal: 0%;
    --clip-vertical: 95%;
  }
  100% {
    --clip-horizontal: 0%;
    --clip-vertical: 0%;
  }
}

This example can be found in the following demo (In combination with Scroll Driven Animations):

Animating Background-Image Gradients

Animating the colors of a background-image gradient is unfortunately something that isn’t possible by default. However, by defining the gradient colors using @property, we can finally do this:

By animating the --from-color and --to-color variables, we can create visually appealing transitions between gradient colors, This would be that basic idea:

@property --from-color {
  syntax: "<color>";
  initial-value: #f0f;
  inherits: false;
}

@property --to-color {
  syntax: "<color>";
  initial-value: #0ff;
  inherits: false;
}

.item {
  background: linear-gradient(to right, var(--from-color), var(--to-color));
  animation: gradient 4s linear infinite;
}


@keyframes gradient {
  0%,
  100% {
    --from-color: #f0f;
    --to-color: #0ff;
  }
  50% {
    --from-color: #00f;
    --to-color: #ff0;
  }
}

Here is that in action:

Or take a look at the following example where I animate another gradient (with kudos to gradient.style)

It is a game changer

This is really going to be a fantastic addition for those of us that love creating design systems and fancy animations. Remember that these are just some basic examples. I’ve seen quite a few examples out there by far greater minds than myself where even numbers were converted to integers. Really looking forward to what this will bring, whether that is for animations or just really clever typed design systems.

I really went the extra mile with writing this as I hope that for some this can be a great entry point. To be honest, this was an entry point for myself as I was doing research while writing this article. Not bad for a first article of the year, not bad at all….

Let 2024 bring you lots of happiness, love and beautiful splashes of CSS once again. May you be joyful, playful and keep on coding the amazing things you do.

 in  css