Making Custom Properties (CSS Variables) More Dynamic

Avatar of Dan Wilson
Dan Wilson on

DigitalOcean provides cloud products for every stage of your journey. Get started with $200 in free credit!

CSS Custom Properties (perhaps more easily understood as CSS variables) provide us ways to make code more concise, as well as introduce new ways to work with CSS that were not possible before. They can do what preprocessor variables can… but also a lot more. Whether you have been a fan of the declarative nature of CSS or prefer to handle most of your style logic in JavaScript, Custom Properties bring something to the table for everyone.

Most of the power comes from two unique abilities of Custom Properties:

  • The cascade
  • The ability to modify values with JavaScript

Even more power is exposed as you combine Custom Properties with other preexisting CSS concepts, like calc() .

The Basics

You can use Custom Properties to do effectively what variables in preprocessors like Sass provide – set a global or scoped variable value, and then use it later in your code. But thanks to the cascade, you can give new property values inside a more specific rule.

This cascading can lead to several interesting approaches, as shown by Violet Peña with an overview of the key benefits of variables and Chris with a roundup of site theming options.

People have been discussing these benefits from the cascade for a few years now, but it often gets lost in the conversation despite being a key functionality that differentiates it from preprocessors. Amelia Bellamy-Royds discussed it in the context of SVG and use in 2014, and Philip Walton noted a lot of these general cascading benefits in 2015, and last year Gregor Adams showed how they can be used in a minimal grid framework. Taking advantage of the cascade is likely the easiest way to start working with Custom Properties with progressive enhancement in mind.

Okay. Now that we know Custom Properties can natively give us some functionality preprocessors have and some new uses thanks to the cascade – do they give us anything we simply never could do before?

You bet!

Individualizing Properties

All of the properties that have multiple parts are able to be used differently now. Multiple backgrounds can be separated, and multiple transition-durations can be broken out individually. Instead of taking a rule like transform: translateX(10vmin) rotate(90deg) scale(.8) translateY(5vmin) you can set one rule with several custom properties and change the values independently thereafter.

.view { 
  transform: 
    translateX(var(--tx, 0))
    rotate(var(--deg, 0))
    scale(var(--scale, 1))
    translateY(var(--ty, 0));
}
.view.activated {
  --tx: 10vmin;
  --deg: 90deg;
}
.view.minimize {
  --scale: .8;
}
.view.priority {
  --ty: 10vmin;
}

It takes a bit to initialize, but then that little bit of extra effort up front sets you up to modify each transform function independently based on the needs of the class/selector rule. Your markup can then include any or all of the classes defined on each .view element and the transform will update appropriately.

While independent transform properties are coming (and at that time translate, scale, and rotate will be first level citizens), they are currently only in Chrome behind a flag. With Custom Properties you can get this functionality today with more support (and the additional ability to define your own order of functions, since rotate(90deg) translateX(10vmin) is different than translateX(10vmin) rotate(90deg), for example).

If you are okay with them sharing the same timing options, they can even animate smoothly when using transition when changing any of the variables. It’s kind of magical.

See the Pen CSS Variables + Transform = Individual Properties (with Inputs) by Dan Wilson (@danwilson) on CodePen.

Going from No Units to All the Units

You can build on these concepts when combining with calc(). Instead of always setting variables as above with units (--card-width: 10vmin or --rotation-amount: 1turn) you can drop the units and use them in more places with a relation to one another. Now the values in our Custom Properties can be more dynamic than they already have been.

While calc() has been around for a few years now, it has arguably been most useful when trying to get a result from adding values with different units. For example, you have a fluid width in percentage units that needs to be shortened by 50px (width: calc(100% - 50px) ). However, calc() is capable of more.

Other operations like multiplication are allowed inside calc to adjust a value. The following is valid and gives us a sense that the transforms and filters are related to one another since they all use the number 10.

.colorful {
  transform: 
    translateX(calc(10 * 1vw))
    translateY(calc(10 * 1vh));
  filter: hue-rotate(calc(10 * 4.5deg));
}

This likely isn’t as common a use case because it is a calculation you don’t need the browser to compute. 10 * 1vw will always be 10vw so the calc gives us nothing. It can be useful when using a preprocessor with loops, but that is a smaller use case and can typically be done without needing CSS calc() .

But what if we replace that repeated 10 with a variable? You can base values from a single value in multiple places, even with different units as well as open it up to change values in the future. The following is valid thanks to unitless variables and calc:

.colorful {
  --translation: 10;
  transform: 
    translateX(calc(var(--translation) * 1vw))
    translateY(calc(var(--translation) * 1vh));
  filter: hue-rotate(calc(var(--translation) * 4.5deg));

  will-change: transform, filter;
  transition: transform 5000ms ease-in-out, filter 5000ms linear;
}

.colorful.go {
  --translation: 80;
}

See the Pen Single Custom Property, Multiple Calcs by Dan Wilson (@danwilson) on CodePen.

The single value can be taken (initially 10, or later changed to 80… or any other number) and applied separately to vw units or vh units for a translation. You can convert it to deg for a rotation or a filter: hue-rotate().

You don’t have to drop the units on the variable, but as long as you have them in your calc you can, and it opens up the option to use it in more ways elsewhere. Animation choreography to offset durations and delays can be accomplished by modifying the base value in different rules. In this example we always want ms as our end unit, but the key result we want is for our delay to always be half the animation’s duration . We then can do this by modifying only our --duration-base.

See the Pen Delay based on Duration by Dan Wilson (@danwilson) on CodePen.

Even cubic beziers are up for Custom Properties modification. In the following example, there are several stacked boxes. Each one has a slightly smaller scale, and each is given a cubic bezier multiplier. This multiplier will be applied individually to the four parts of a baseline cubic-bezier. This allows each box to have a cubic bezier that is different but in relation to one another. Try removing or adding boxes to see how they play with one another. Press anywhere to translate the boxes to that point.

See the Pen Spiral Trail… Kinda by Dan Wilson (@danwilson) on CodePen.

JavaScript is used to randomize the baseline on each press, as well as setting up each box’s multiplier. The key part of the CSS, however, is:

.x {
  transform: translateX(calc(var(--x) * 1px));
  /* baseline value, updated via JS on press */
  transition-timing-function: 
    cubic-bezier(
      var(--cubic1-1),
      var(--cubic1-2),
      var(--cubic1-3),
      var(--cubic1-4));
}
.advanced-calc .x {
  transition-timing-function: 
    cubic-bezier(
      calc(var(--cubic1-1) * var(--cubic1-change)),
      calc(var(--cubic1-2) * var(--cubic1-change)),
      calc(var(--cubic1-3) * var(--cubic1-change)),
      calc(var(--cubic1-4) * var(--cubic1-change)));
}

If you are viewing this in certain browsers (or are wondering why this example has an .advanced-calc class) you might already suspect there is an issue with this approach. There is indeed an important caveat… calc magic does not always work as expected across the browsers. Ana Tudor has long discussed the differences in browser support for calc , and I have an additional test for some other simplified calc use cases.

The good news: All the browsers that support Custom Properties also largely work with calc when converting to units like px, vmin, rem, and other linear distance units inside properties such as width and transform: translate().

The not-so-good news: Firefox and Edge often have problems with other unit types, such as deg, ms , and even % in some contexts. So the previous filter: hue-rotate() and --rotation properties would be ignored. They even have problems understanding calc(1 * 1) in certain cases so even remaining unitless (such as inside rgb()) can be a problem.

While all the browsers that support Custom Properties will allow variables inside our cubic-bezier , not all of them allow calc at any level. I feel these calc issues are the main limiting factors with Custom Properties today… and they’re not even a part of Custom Properties.

There are bugs tracked in the browsers for these issues, and you can work around them with progressive enhancement. The earlier demos only do the cubic-bezier modifications if it knows it can handle them, otherwise you get the baseline values. They will erroneously pass a CSS @supports check, so a JS Modernizr-style check is needed:

function isAdvancedCalcSupported() {
  document.body.style.transitionTimingFunction = 'cubic-bezier(calc(1 * 1),1,1,1)';
  return getComputedStyle(document.body).transitionTimingFunction != 'ease';
  //if the browser does not understand it, the computed value will be the default value (in this case "ease")
}

Interacting via JavaScript

Custom Properties are great for what they provide in CSS, but more power is unlocked when you communicate via JavaScript. As shown in the cubic-bezier demo, we can write a new property value in JavaScript:

var element = document.documentElement;
element.style.setProperty('--name', value);

This will set a new value for a globally defined property (in CSS defined in the :root rule). Or you can go more direct and set a new value for a specific element (and thus give it the highest specificity for that element, and leave the variable unchanged for other elements that use it). This is useful when you are managing state and need to modify a style based on given values.

David Khourshid has discussed powerful ways to interact with Custom Properties via JS in the context of Observables and they really fit together nicely. Whether you want to use Observables, React state changes, tried-and-true event listeners, or some other way to derive value changes, a wide door is now open to communicate between the two.

This communication is especially important for the CSS properties that take multiple values. We’ve long had the style object to modify styles from JavaScript, but that can get complicated as soon as we need to modify only one part of a long value. If we need to change one background out of ten that are defined in a background rule, we have to know which one we are modifying and then make sure we leave the other nine alone. This gets even more complicated for transform rules when you are trying to only modify a rotate() and keep the current scale() unchanged. With Custom Properties you can use JavaScript to modify each individually, simplifying the state management of the full transform property.

See the Pen Dance of the Hexagons and Variables by Dan Wilson (@danwilson) on CodePen.

The unitless approach works well here, too. Your setProperty() calls can pass raw numbers to CSS instead of having to append units, which can simplify your JavaScript in some cases.

Is it Time to Use This?

As Custom Properties are now in the latest browsers from Mozilla, Google, Opera, Apple, and Microsoft – it’s definitely a good time to explore and experiment. A lot of what is discussed here can be used now with sensible fallbacks in place. The calc updates needed in some of the browsers are further out, but there are still times when you can reasonably use them. For example, if you work on hybrid mobile apps that are limited to more recent iOS, Android, or Windows versions you will have more room to play.

Custom Properties present a big addition to CSS, and it can take some time to wrap your head around how it all works. Dip your toes in, and then dive in if it suits you.