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 background
s can be separated, and multiple transition-duration
s 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.
All the above are not working in Edge browser. Css Varable not work in Edge browser. How to solve this
Edge 15 has limited support for this. Check this link for support of CSS Variables: http://caniuse.com/#search=var
Edge requires the latest version that came out last month (v15) with the Windows 10 Creators Update. The article calls out a few caveats with it, though there is also an additional issue when combining translate + variables + calc which prevents one of the demos from working at all. The Edge team is working on several of these less common CSS Variables issues for their next release.