Originally spec’d out by the W3C in 2012, Custom Properties were updated in 2014 with significant syntax improvement. Initially implemented only by Chrome and Firefox, it was only in 2016 that it reached general availability, with Microsoft being the only one missing support. That also inevitably changed with the release of Edge in January 2020.

Browser support information from caniuse in 2021.

So, this brings us to the current day. What is the status of the Custom Properties API, how have we been using it, and what can we expect from it in the future? These are all questions that will be addressed in the PART I of this article, while in PART II we will dive in on how to use Custom Properties effectively with Design Systems.

PART I — Definition, Boundaries, and Beyond

Definition — From Basic to Advanced Use Cases

What Are CSS Custom Properties?

Custom properties, often referred to as CSS variables (although technically both concepts exist), are entities defined by CSS authors that contain specific values to be reused throughout a document. They are more or less similar to their JavaScript counterparts and, in my opinion, a considerable improvement to the ones provided by CSS preprocessors like SASS.

Why, you may ask?

  • They are browser native; you can use them without the need of a preprocessor.
  • They cascade. You can set a variable inside any selector to set or override its current value.
  • When their values change (e.g. media query or other state), the browser repaints as needed in runtime.
  • You can access and manipulate them in JavaScript.

css custom properties

Basic Usage

CSS variables add two new features to our CSS toolbox

  • The ability for an author to assign arbitrary values to a property with an author-chosen name.
  • The var() function, which allows an author to use these values in other properties.

Note: variables names are case sensitive — --my-color will be treated as a separate custom property to --My-color.

Here’s a quick example to demonstrate:

:root {
--color-primary: #1068eb;
}
.card {
color: var(--color-primary);
}

CSS Variables and JavaScript

This is where things can start looking even more interesting, for more complex use-cases! This allows us to use all the JavaScript power to do all sorts of calculations based on information from the DOM, and then save that on a CSS variable that can be used on the CSS file.

To manipulate custom properties in JavaScript, it is just like any other standard property.

// get variable from inline style
element.style.getPropertyValue("--my-var");
// get variable from wherever
getComputedStyle(element).getPropertyValue("--my-var");
// set variable on inline style
element.style.setProperty("--my-var", jsVar + 4);

On the OutSystems UI, we have been using this technique on some patterns, as together with the reactive runtime, it can be extremely powerful.

We simply pass the values from the input parameters to a JavaScript node on the patterns’s OnReady and OnParameterChange, where we create CSS Variables to be used on the CSS (see image below).

This way, the parameter changes are instantly reflected in the runtime, without the need for a page reload. The same for CSS, as it will always read the current value of the CSS variables used.

Code architecture example from the Gallery Pattern.

Remember, as we are setting the variable on the gallery element in JavaScript, it will remain at the pattern scope. This means that each Gallery pattern on a screen will get the value from the variable on it’s own scope!

Boundaries — What You Can’t Do with CSS Variables

Although CSS Variables have come a long way and currently support a vast array of use cases, there are still some minor limitations to be considered.

Media-Queries Breakpoints

Please note, this isn’t referring to using CSS variables inside media queries. That’s perfectly supported and very common, allowing a redefining of variables values on different devices.

The limitation here is for using variables on the breakpoint value. This is because we set variables on elements, mainly on the htmlitself using :root, and from there to be inherited to other elements. But a media query is not an element, it does not inherit from html, so it can't work.

:root {
--mobile-breakpoint: 642px;
}
@media (max-width: var(--mobile-breakpoint)) {
}

This doesn’t work!

Inline Variables on Reactive Runtime

Although the CSS Variables specification supports usage on HTML inline, that isn’t possible on the OutSystems Reactive runtime. The reason for this is a limitation on React itself, which seems to have been fixed on recent versions, but it’s still not available on the version used by the OutSystems platform.

Why would this be useful, as inline styles should generally be avoided? For two reasons:

  • CSS Variables aren’t properly a style, and not something that generally needs to be overridden on the CSS file. They are just information on some element’s scope.
  • It would allow us to skip the JavaScript node step on the Gallery example shown previously. We could simply pass the values from the input parameter directly on the inline style of the element.
If inline variables were possible in Reactive, we could skip the JavaScript.

Animating Variables

Directly animating variables isn’t possible, as they have no type that the browser can interpret. You can however animate properties whose values are variables.

To further clarify, the browser knows how to animate opacity or transform properties, but not your --color-primary variable, as it’s up to you to define its value, but you have no way to define a type (the browser doesn’t know if your variable is an integer, a color, or a string).

That being said, there's a new API — CSS HOUDINI — that will help us with this. We will briefly explain that further ahead in the article.

Beyond — Peeking Into the Future

“Fear is the mind killer”, Muad'dib (Dune, 1984, Lynch, D.).

CSS variables are far from a finished API; they keep growing and evolving, and there have been some promising new features that, although not yet fully supported and consolidated, have started to be used and pushed, particularly by the Google Dev Team. Here are some rapid-fire mentions on some of those features!

Higher Level Custom Properties

Although still only a draft idea, proposed by Lea Verou at the CSS WG Telecon from early December 2020, this new feature would be very useful to have a CSS Variable control a varying set of other properties. In other words, have a feature similar to an JavaScript If statement, where changing the value of the high level variable, would affect all other consumer variables.

As an example from this Bramus’s article, we could use a --size variable to control a set of different behaviors:

@if (var(--size) = big) { width: 16vw; padding: 1vw; }

CSS Houdini — Properties and Values API

CSS Houdini is an umbrella term that covers a set of low-level APIs that expose parts of the CSS rendering engine, and give developers access to the CSS Object Model.

One of the most exciting additions on this umbrella is the Properties and Values API. This API enhances your CSS Variables, allowing property type checking, default values, and properties that do or do not inherit their value.

You can define them in Javascript:

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

Or in the CSS itself:

@property --my-color { syntax: ''; inherits: false; initial-value: #c0ffee; }

Remember that limitation on animating CSS Variables? This is now solved, as they can have a type (through the syntax property — check the full list here), informing the browser of what is indeed animating.

Check this codepen example with a simple color transition on hover:

See the Pen Houdini Properties API by Bernardo Cardoso (@BenCardoso) on CodePen.

PART II — Using CSS Variables to Build and Scale Design Systems

In the Dune Universe, the Spice is the most important resource. It only exists on the desert planet Arrakis, and, besides providing some mystical capabilities, more importantly it’s essential for space travel. Whoever controls the Spice, controls the Universe!

On the left: dunecat meme (source: unknown). On the right: book cover of Dune (HERBERT, F., 1965).

The same can be said for CSS Variables on a Design System (wow...what a smooth transition!); whoever controls them, controls the overall styles of that project. Or at least, that’s how it should be, if the architecture is correctly defined and maintained!

Since the release of the OutSystems UI Web and later on the OutSystems UI for reactive, we have been using CSS Variables on our themes. It was a major improvement on what we had before, as it came along with our new Design System. Let's take a closer look at how and why!

Design Tokens and the :root

First, let’s talk a bit about Design Tokens. If you’re unfamiliar with the term, it was created by Jina Anne, from Salesforce, and in her own words they are defined as:

“(...) visual atoms of the design system – specifically, they are named entities that store visual design attributes. We use them in place of hard–coded values in order to maintain a scalable and consistent visual system.”

These visual attributes are generally such as values for margins and spacing, font sizes and families, or colors values.

Design tokens are really useful because they allow our systems to be scalable and with a single source of truth. This source is generally as technology abstracted as possible; it could even be an excel file or a JSON (tools can then be used to automatically import them to Sketch/Figma and the CSS).

Ultimately they will reach the CSS through CSS Variables on the :root element (the html element) and some utility classes.

This is what you see on the OutSystems UI theme, where we define the main Design Tokens from our Design Systems on the :root, using CSS Variables for typography, colors, sizes, border, shadow and other settings related to main element’s sizes and notch spacings.

Generally speaking, these global variables should represent static values.

OutSystems UI Design Tokens as CSS Variables on the :root element.

CSS Variables Tiers

The intention is that these variables flow throughout the CSS, being used on each pattern or element, instead of their fixed values. This ensures that the control of the Design System is at the :root level!

“The Spice Must Flow” (Dune, 2021, Villeneuve, D., Warner Media Company).

Tier 1 — The Globals

The first tier of complexity is equivalent to what we just saw, combined with these :root variables’s direct usage throughout the CSS. Here’s an example from the OutSystems UI’s Card:

.card { background-color: var(--color-neutral-0); border-radius: var(--border-radius-soft); border: var(--border-size-s) solid var(--color-neutral-4); padding: var(--space-m); }

OutSystems UI Card.

It’s totally dependent on the variables defined in the :root. This, of course, guarantees that any changes to the Design Tokens values are reflected automatically on the pattern.

Tier 2 — Pattern Scope

Next, there’s the local scope of the pattern. It could mean two different type of variables defined:

  • Variables with dynamic values exclusive to the pattern behavior or a more complex UI;
  • Variables with static values exclusive to the pattern, based by default on the global variables;

The first are essentially what we already saw previously with the Gallery example. Dynamic variables that are defined inline or using JavaScript. On another example from OutSystems UI, we have the Floating Actions pattern:

Floating Actions pattern from OutSystems UI.

Here, we set a -delay variable on JavaScript equal to the current Floating Actions Item index. Then, on the CSS we use it on the transition to simulate a sequential animation when opening the pattern.

.floating-actions-wrapper.is--open .floating-actions-item {
opacity: 1;
transform: translateY(0px) translateZ(0) scale(1);
transition: all 180ms ease-out;
transition-delay: calc(var(--delay) * 40ms);
}

 

The second type of variable is rarer and something that we don’t have on the OutSystems UI. The idea is that each pattern should have it’s own variables, even if by default they are based on the global ones.

Let’s apply this on our Card friend again, to make it clearer:

.card {
--card-background: var(--color-neutral-0);
--card-border-radius: var(--border-radius-soft);
--card-border-size: var(--border-size-s);
--card-border-color: var(--color-neutral-4);
--card-padding: var(--space-m);

background-color: var(--card-background);
border-radius: var(--card-border-radius);
border: var(--card-border-size) solid var(--card-border-color);
padding: var(--card-padding);
}

Using this type of architecture brings a much more modular and scalable approach. Specially if you have some kind of theme editor tool, this allows you to create exceptions more easily.

Another relevant point is that each pattern style can be manipulated through JavaScript if needed, allowing for greater extensibility for different kinds of frontend technologies used, while keeping the same stylesheet.

As for negative points, clearly the main one is the increase of complexity using this kind of architecture. The number of CSS Variables will increase exponentially, which can become difficult to maintain and document.

That being said, as an example, the Ionic Framework just for the button has over 20 CSS Variables! That might sound like too much for you, but it probably makes sense for their needs and they have the architecture to support it. As with everything, there’s a balance that should guide those decisions.

Tier 3 — The Shadow DOM

“But the sun is eclipsed by the moon” (lyrics from Eclipse, Pink Floyd, © BMG Rights Management. Image from Dune, 2021, Warner Media Company).

Finally, we can even go a layer deeper, if you’re using Web Components with Shadow DOM!

Web Components are a new browser feature, allowing you to create reusable custom HTML elements — with their functionality encapsulated away from the rest of your code — and utilize them across your projects. If you want to know more, you can check this article I wrote trying to summarize its main features (shameless self-promote) or the Google Dev documentation!

CSS Variables have a major role on the styling of Web Components, being almost the only way to style a custom element’s Shadow DOM from an external stylesheet. This means that if you are building Web Components, the CSS architecture and general definition of your CSS Variables should be carefully considered to ensure a clean and scalable library of patterns, otherwise the customization of these components can become difficult to reach.

Code example for styling the Shadow DOM.

Wrap Up

In conclusion, we looked back at the general progress that the Custom Properties API had throughout the last decade, from a low supported new feature, to a frontend standard.

At OutSystems we have been using them since the release of OutSystems UI Web in 2018, and there’s a monumental difference in the amount of work it used to take to customize the old Silk UI and the new frameworks. It was never so easy to bootstrap a Live Style Guide, with your company’s brand!

But, as we saw, it doesn’t end here. More features are being added, like the CSS Houdini’s Properties & Values, that promise an exciting future not only for CSS Variables, but for how we style the Web!

We just need to balance our inherent fear of trying and adopting something. Remember the words of Paul Atreides:

“Fear is the Mind Killer!”
Paul Atreides in Dune (HERBERT, F., 1965).