Typed custom properties with the CSS Properties and Values API

CSS custom properties are a practical way to keep stylesheets DRY, but they come with limitations: any value is accepted, so a property intended for one purpose can be accidentally overwritten with something invalid; values inherit by default; and you can't transition or animate them. The CSS Properties and Values API Level 1, part of the Houdini effort and available in Chrome 78, addresses these gaps by letting you register custom properties with a defined type, inheritance behavior, and initial value. Once registered, the browser can validate, compute, and even animate them like native properties.

What Houdini brings to CSS

Houdini is a set of specifications from the CSS-TAG Houdini Task Force aimed at exposing the browser's rendering engine to developers. The goal is to enable typed CSS values in JavaScript, performant polyfills, and the ability to extend CSS itself without hitting the usual performance walls. The Paint API has already shipped; the Properties and Values API is the next piece of that puzzle.

Registering a property

Without registration, a custom property like --my-color can be set to any value, including one that doesn't make sense for its intended use. Consider this stylesheet:

.thing {
  --my-color: green;
}

If --my-color is later set to a URL instead of a color, there's nothing in the browser to flag the mistake. When the property is consumed by color or background, the invalid value simply falls back to the default (black for text, transparent for background).

.thing {
  --my-color: url('not-a-color');
  color: var(--my-color);
}

With the CSS Properties and Values API, that same property can be given a type. Once registered, the browser knows that --my-color is a <color> and will only accept values that match that syntax.

Property registration looks like this:

window.CSS.registerProperty({
  name: '--my-color',
  syntax: '<color>',
  inherits: false,
  initialValue: 'black',
});

Each option plays a specific role:

  • name: the custom property's name.
  • syntax: a string describing how the property's value should be parsed. Possible values are defined in the CSS Values and Units specification. The default is *, which accepts anything.
  • inherits: a boolean determining whether the property inherits from its parent. The default is true.
  • initialValue: the property's starting value.

The syntax string supports more than single types. You can modify it in a few ways:

  • Append + for a space-separated list, so <length>+ accepts a list of lengths.
  • Append # for a comma-separated list, so <color># accepts a comma-separated list of colors.
  • Combine options with | to allow any of them, for example <color># | <url> | magic.

Two caveats to keep in mind

There are a couple of limitations. First, once a property is registered, you can't update or re-register it — attempting to do so throws an error. The registration is effectively permanent for the lifetime of the page.

Second, registered properties are validated at computed-value time, not at parse time. That means an invalid value won't show up as an error when you inspect the element's styles, and an invalid declaration that follows a valid one won't be ignored or fall back in the way you might expect. Instead, an invalid value falls back to the property's initialValue.

Animating registered custom properties

Type registration has a useful side effect: it makes custom properties animatable. When the browser knows the syntax of a property, it can interpolate between values. A simple example:

<script>
CSS.registerProperty({
  name: '--stop-color',
  syntax: '<color>',
  inherits: false,
  initialValue: 'blue',
});
</script>

<style>
button {
  --stop-color: red;
  transition: --stop-color 1s;
}

button:hover {
  --stop-color: green;
}
</style>

Here the button will transition its --my-color value from red to green on hover, because the registered property tells the browser it's a color with a defined sequence of intermediate states. Without that registration, the same stylesheet would snap abruptly between values.

The same principle extends to gradients. Because a linear-gradient can reference a registered custom property, animating that property animates the gradient itself:

button {
  --stop-color: red;
  background: linear-gradient(var(--stop-color), black);
  transition: --stop-color 1s;
}

button:hover {
  --stop-color: green;
}

That snippet animates the custom property used inside the gradient, producing a smooth shift from one gradient to another.

What's next

Houdini's specifications are still making their way through browsers, but the progress is tangible. With the Paint API and now the CSS Properties and Values API available, developers can define typed CSS properties, use them in new ways, and animate them without JavaScript. The Houdini issue queue is open for feedback on what should come next in the specification effort.